From 0ea023047d1417ea829458f3f4455cc5bf016681 Mon Sep 17 00:00:00 2001 From: Philippe Simons Date: Mon, 11 Mar 2019 13:22:33 +0100 Subject: [PATCH 001/807] add WorkManager extension --- core_settings.gradle | 2 + extensions/workmanager/README.md | 23 +++ extensions/workmanager/build.gradle | 49 ++++++ .../workmanager/src/main/AndroidManifest.xml | 18 ++ .../ext/workmanager/WorkManagerScheduler.java | 164 ++++++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 extensions/workmanager/README.md create mode 100644 extensions/workmanager/build.gradle create mode 100644 extensions/workmanager/src/main/AndroidManifest.xml create mode 100644 extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java diff --git a/core_settings.gradle b/core_settings.gradle index 4d90fa962a..38889e1a21 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-jobdispatcher' +include modulePrefix + 'extension-workmanager' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') +project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager') diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md new file mode 100644 index 0000000000..61288681e8 --- /dev/null +++ b/extensions/workmanager/README.md @@ -0,0 +1,23 @@ +# ExoPlayer WorkManager extension # + +This extension provides a Scheduler implementation which uses [Android Arch WorkManager][]. + +[Android Arch 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..8580638896 --- /dev/null +++ b/extensions/workmanager/build.gradle @@ -0,0 +1,49 @@ +/* + * 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 + } + + testOptions.unitTests.includeAndroidResources = true +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation 'android.arch.work:work-runtime:1.0.0' +} + +ext { + javadocTitle = 'Android Arch WorkManager extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-workmanager' + releaseDescription = 'Android Arch 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..f6fa0df5aa --- /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..d125b76796 --- /dev/null +++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java @@ -0,0 +1,164 @@ +/* + * 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.workmanager; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +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; + +import androidx.annotation.NonNull; +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; + +/*** + * A {@link Scheduler} that uses {@link WorkManager}. + */ +public final class WorkManagerScheduler implements Scheduler { + + 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.KEEP, 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(); + + switch (requirements.getRequiredNetworkType()) { + case Requirements.NETWORK_TYPE_NONE: + builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED); + break; + case Requirements.NETWORK_TYPE_ANY: + builder.setRequiredNetworkType(NetworkType.CONNECTED); + break; + case Requirements.NETWORK_TYPE_UNMETERED: + builder.setRequiredNetworkType(NetworkType.UNMETERED); + break; + default: + throw new UnsupportedOperationException(); + } + + if (requirements.isChargingRequired()) { + builder.setRequiresCharging(true); + } + + if (requirements.isIdleRequired() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setRequiresDeviceIdle(true); + } + + return builder.build(); + } + + 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. */ + public static final class SchedulerWorker extends Worker { + + private final WorkerParameters workerParams; + private final Context context; + + public SchedulerWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + this.workerParams = workerParams; + this.context = context; + } + + @NonNull + @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(); + } + } + } +} From f81efde47637c614ce944a959eab4a6919771970 Mon Sep 17 00:00:00 2001 From: Philippe Simons Date: Tue, 12 Mar 2019 10:14:47 +0100 Subject: [PATCH 002/807] replace current Work since requirements may have changed --- .../exoplayer2/ext/workmanager/WorkManagerScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index d125b76796..0b85cc1546 100644 --- 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 @@ -62,7 +62,7 @@ public final class WorkManagerScheduler implements Scheduler { Data inputData = buildInputData(requirements, servicePackage, serviceAction); OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData); logd("Scheduling work: " + workName); - WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest); + WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest); return true; } From 70530cd1e6000ceb3b99dfaf84bfb6de0cce7943 Mon Sep 17 00:00:00 2001 From: toxicbakery Date: Thu, 4 Apr 2019 15:39:19 -0400 Subject: [PATCH 003/807] #5731 Add license information to generated POM files --- build.gradle | 1 + publish.gradle | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/build.gradle b/build.gradle index f8326dd503..6ae3ccf2e6 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ allprojects { } buildDir = "${externalBuildDir}/${project.name}" } + group = 'com.google.android.exoplayer' } apply from: 'javadoc_combined.gradle' diff --git a/publish.gradle b/publish.gradle index 85cf87aa85..e82cf32552 100644 --- a/publish.gradle +++ b/publish.gradle @@ -23,6 +23,20 @@ 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 +44,21 @@ 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)) + def printer = new XmlNodePrinter(writer) + printer.preserveWhitespace = true + printer.print(xml) + writer.close() +} From a2282ad0ddf9d2f56f21323f9c755e458949577d Mon Sep 17 00:00:00 2001 From: loki666 Date: Tue, 16 Apr 2019 18:26:32 +0200 Subject: [PATCH 004/807] PR comments update --- extensions/workmanager/README.md | 4 ++-- extensions/workmanager/build.gradle | 6 +++--- .../exoplayer2/ext/workmanager/WorkManagerScheduler.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md index 61288681e8..e1d122b84b 100644 --- a/extensions/workmanager/README.md +++ b/extensions/workmanager/README.md @@ -1,8 +1,8 @@ # ExoPlayer WorkManager extension # -This extension provides a Scheduler implementation which uses [Android Arch WorkManager][]. +This extension provides a Scheduler implementation which uses [WorkManager][]. -[Android Arch WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html +[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html ## Getting the extension ## diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 8580638896..1d017e4ff2 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,16 +34,16 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'android.arch.work:work-runtime:1.0.0' + implementation 'android.arch.work:work-runtime:1.0.1' } ext { - javadocTitle = 'Android Arch WorkManager extension' + javadocTitle = 'WorkManager extension' } apply from: '../../javadoc_library.gradle' ext { releaseArtifact = 'extension-workmanager' - releaseDescription = 'Android Arch WorkManager extension for ExoPlayer.' + releaseDescription = 'WorkManager extension for ExoPlayer.' } apply from: '../../publish.gradle' 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 index 0b85cc1546..4d92560465 100644 --- 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 @@ -35,7 +35,7 @@ import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; -/*** +/** * A {@link Scheduler} that uses {@link WorkManager}. */ public final class WorkManagerScheduler implements Scheduler { From 1c7cbef1b92f6199a8c9cc98fedf7f8de4980058 Mon Sep 17 00:00:00 2001 From: Philippe Simons Date: Wed, 17 Apr 2019 12:41:10 +0200 Subject: [PATCH 005/807] use Util.SDK_INT use androidX workmanager --- extensions/workmanager/build.gradle | 2 +- .../exoplayer2/ext/workmanager/WorkManagerScheduler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 1d017e4ff2..8e2c8fc423 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'android.arch.work:work-runtime:1.0.1' + implementation 'androidw.work:work-runtime:2.0.1' } ext { 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 index 4d92560465..0df75c495b 100644 --- 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 @@ -94,7 +94,7 @@ public final class WorkManagerScheduler implements Scheduler { builder.setRequiresCharging(true); } - if (requirements.isIdleRequired() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (requirements.isIdleRequired() && Util.SDK_INT >= 23) { builder.setRequiresDeviceIdle(true); } From 6c5a39ac8277eac88e29614fbcb053a0c791dab2 Mon Sep 17 00:00:00 2001 From: Philippe Simons Date: Wed, 17 Apr 2019 12:42:52 +0200 Subject: [PATCH 006/807] android X --- extensions/workmanager/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 8e2c8fc423..41145c0d20 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidw.work:work-runtime:2.0.1' + implementation 'androidx.work:work-runtime:2.0.1' } ext { From 7d5558881df6509801678ab3e21ccb12490ee32e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 7 May 2019 09:37:33 +0100 Subject: [PATCH 007/807] Fix two ad insertion related bugs in DefaultPlaybackSessionManager. 1. A content session after an ad has been played was not re-marked as active, leading to new ad session being marked as active too early. 2. Switching from content to post-roll ended the content session because the return value of getAdGroupTimeUs of C.TIME_END_OF_SOURCE was not handled. Using the nextAdGroupIndex instead. PiperOrigin-RevId: 246977327 --- demos/main/build.gradle | 2 +- .../exoplayer2/demo/TrackSelectionDialog.java | 2 +- .../res/layout/track_selection_dialog.xml | 4 +- .../google/android/exoplayer2/Timeline.java | 3 +- .../DefaultPlaybackSessionManager.java | 16 ++- .../DefaultPlaybackSessionManagerTest.java | 101 ++++++++++++++++++ 6 files changed, 114 insertions(+), 14 deletions(-) diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 7089d4d731..494af011a5 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -63,7 +63,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' - implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' + implementation 'com.android.support:support-core-ui:' + supportLibraryVersion implementation 'androidx.fragment:fragment:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') 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 a7dd1a0df8..86d01706fb 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 @@ -25,7 +25,7 @@ import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; -import androidx.viewpager.widget.ViewPager; +import android.support.v4.view.ViewPager; import androidx.appcompat.app.AppCompatDialog; import android.util.SparseArray; import android.view.LayoutInflater; diff --git a/demos/main/src/main/res/layout/track_selection_dialog.xml b/demos/main/src/main/res/layout/track_selection_dialog.xml index 7f6c45e131..24d101ae4c 100644 --- a/demos/main/src/main/res/layout/track_selection_dialog.xml +++ b/demos/main/src/main/res/layout/track_selection_dialog.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + adMediaPeriodId.adIndexInAdGroup); } else { - eventTime.timeline.getPeriod(adPeriodIndex, period); - long adGroupTimeMs = - adMediaPeriodId.adGroupIndex < period.getAdGroupCount() - ? C.usToMs(period.getAdGroupTimeUs(adMediaPeriodId.adGroupIndex)) - : 0; // Finished if the event is for content after this ad. - return adGroupTimeMs <= eventTime.currentPlaybackPositionMs; + return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET + || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java index 2993e960b4..f0b18b4a20 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java @@ -598,6 +598,43 @@ public final class DefaultPlaybackSessionManagerTest { assertThat(updatedSessionId300).isEqualTo(sessionId300); } + @Test + public void timelineUpdate_withContent_doesNotFinishFuturePostrollAd() { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + new AdPlaybackState(/* adGroupTimesUs= */ C.TIME_END_OF_SOURCE) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1))); + EventTime adEventTime = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime contentEventTime = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0, + /* nextAdGroupIndex= */ 0)); + sessionManager.updateSessions(contentEventTime); + sessionManager.updateSessions(adEventTime); + + sessionManager.handleTimelineUpdate(contentEventTime); + + verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); + } + @Test public void positionDiscontinuity_withinWindow_doesNotFinishSession() { Timeline timeline = @@ -943,6 +980,70 @@ public final class DefaultPlaybackSessionManagerTest { verifyNoMoreInteractions(mockListener); } + @Test + public void + updateSessions_withNewAd_afterDiscontinuitiesFromContentToAdAndBack_doesNotActivateNewAd() { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + new AdPlaybackState( + /* adGroupTimesUs= */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1))); + EventTime adEventTime1 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime adEventTime2 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventTime contentEventTime1 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0, + /* nextAdGroupIndex= */ 0)); + EventTime contentEventTime2 = + createEventTime( + adTimeline, + /* windowIndex= */ 0, + new MediaPeriodId( + adTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 0, + /* nextAdGroupIndex= */ 1)); + sessionManager.handleTimelineUpdate(contentEventTime1); + sessionManager.updateSessions(contentEventTime1); + sessionManager.updateSessions(adEventTime1); + sessionManager.handlePositionDiscontinuity( + adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.handlePositionDiscontinuity( + contentEventTime2, Player.DISCONTINUITY_REASON_AD_INSERTION); + String adSessionId2 = + sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId); + + sessionManager.updateSessions(adEventTime2); + + verify(mockListener, never()).onSessionActive(any(), eq(adSessionId2)); + } + private static EventTime createEventTime( Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { return new EventTime( From b3ae1d3fedf5b1477d838850df5100884b6df77b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 May 2019 18:05:26 +0100 Subject: [PATCH 008/807] Add option to clear all downloads. Adding an explicit option to clear all downloads prevents repeated database access in a loop when trying to delete all downloads. However, we still create an arbitrary number of parallel Task threads for this and seperate callbacks for each download. PiperOrigin-RevId: 247234181 --- RELEASENOTES.md | 1 + .../offline/DefaultDownloadIndex.java | 13 ++++ .../exoplayer2/offline/DownloadManager.java | 71 +++++++++++++++---- .../exoplayer2/offline/DownloadService.java | 39 ++++++++++ .../offline/WritableDownloadIndex.java | 7 ++ .../offline/DownloadManagerTest.java | 26 +++++++ 6 files changed, 143 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ba615c4b91..6094046b87 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Offline: Add option to remove all downloads. * Decoders: * Prefer codecs that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 06f308d1e9..ef4bd00f20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -233,6 +233,19 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + @Override + public void setStatesToRemoving() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_REMOVING); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + @Override public void setStopReason(int stopReason) throws DatabaseIOException { ensureInitialized(); 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 3bf03dd3e8..ec5ff81d97 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 @@ -133,10 +133,11 @@ public final class DownloadManager { private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_REMOVE_DOWNLOAD = 7; - private static final int MSG_TASK_STOPPED = 8; - private static final int MSG_CONTENT_LENGTH_CHANGED = 9; - private static final int MSG_UPDATE_PROGRESS = 10; - private static final int MSG_RELEASE = 11; + private static final int MSG_REMOVE_ALL_DOWNLOADS = 8; + private static final int MSG_TASK_STOPPED = 9; + private static final int MSG_CONTENT_LENGTH_CHANGED = 10; + private static final int MSG_UPDATE_PROGRESS = 11; + private static final int MSG_RELEASE = 12; private static final String TAG = "DownloadManager"; @@ -446,6 +447,12 @@ public final class DownloadManager { internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget(); } + /** Cancels all pending downloads and removes all downloaded data. */ + public void removeAllDownloads() { + pendingMessages++; + internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget(); + } + /** * Stops the downloads and releases resources. Waits until the downloads are persisted to the * download index. The manager must not be accessed after this method has been called. @@ -652,6 +659,9 @@ public final class DownloadManager { id = (String) message.obj; removeDownload(id); break; + case MSG_REMOVE_ALL_DOWNLOADS: + removeAllDownloads(); + break; case MSG_TASK_STOPPED: Task task = (Task) message.obj; onTaskStopped(task); @@ -797,6 +807,36 @@ public final class DownloadManager { syncTasks(); } + private void removeAllDownloads() { + List terminalDownloads = new ArrayList<>(); + try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) { + while (cursor.moveToNext()) { + terminalDownloads.add(cursor.getDownload()); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load downloads."); + } + for (int i = 0; i < downloads.size(); i++) { + downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING)); + } + for (int i = 0; i < terminalDownloads.size(); i++) { + downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING)); + } + Collections.sort(downloads, InternalHandler::compareStartTimes); + try { + downloadIndex.setStatesToRemoving(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + ArrayList updateList = new ArrayList<>(downloads); + for (int i = 0; i < downloads.size(); i++) { + DownloadUpdate update = + new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + syncTasks(); + } + private void release() { for (Task task : activeTasks.values()) { task.cancel(/* released= */ true); @@ -1057,16 +1097,7 @@ public final class DownloadManager { // to set STATE_STOPPED either, because it doesn't have a stopReason argument. Assertions.checkState( state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); - return putDownload( - new Download( - download.request, - state, - download.startTimeMs, - /* updateTimeMs= */ System.currentTimeMillis(), - download.contentLength, - /* stopReason= */ 0, - FAILURE_REASON_NONE, - download.progress)); + return putDownload(copyDownloadWithState(download, state)); } private Download putDownload(Download download) { @@ -1120,6 +1151,18 @@ public final class DownloadManager { return C.INDEX_UNSET; } + private static Download copyDownloadWithState(Download download, @Download.State int state) { + return new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress); + } + private static int compareStartTimes(Download first, Download second) { return Util.compareLong(first.startTimeMs, second.startTimeMs); } 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 fdd7163a2c..3900dc8e93 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 @@ -77,6 +77,16 @@ public abstract class DownloadService extends Service { public static final String ACTION_REMOVE_DOWNLOAD = "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + /** + * Removes all downloads. Extras: + * + *
    + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_REMOVE_ALL_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS"; + /** * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: * @@ -296,6 +306,19 @@ public abstract class DownloadService extends Service { .putExtra(KEY_CONTENT_ID, id); } + /** + * Builds an {@link Intent} for removing all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveAllDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground); + } + /** * Builds an {@link Intent} for resuming all downloads. * @@ -414,6 +437,19 @@ public abstract class DownloadService extends Service { startService(context, intent, foreground); } + /** + * Starts the service if not started already and removes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveAllDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + /** * Starts the service if not started already and resumes all downloads. * @@ -560,6 +596,9 @@ public abstract class DownloadService extends Service { downloadManager.removeDownload(contentId); } break; + case ACTION_REMOVE_ALL_DOWNLOADS: + downloadManager.removeAllDownloads(); + break; case ACTION_RESUME_DOWNLOADS: downloadManager.resumeDownloads(); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index ae634f8544..dc7085c85e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -44,6 +44,13 @@ public interface WritableDownloadIndex extends DownloadIndex { */ void setDownloadingStatesToQueued() throws IOException; + /** + * Sets all states to {@link Download#STATE_REMOVING}. + * + * @throws IOException If an error occurs updating the state. + */ + void setStatesToRemoving() throws IOException; + /** * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index a3df647efe..3a3067853e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -243,6 +243,27 @@ public class DownloadManagerTest { downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test + public void removeAllDownloads_removesAllDownloads() throws Throwable { + // Finish one download and keep one running. + DownloadRunner runner1 = new DownloadRunner(uri1); + DownloadRunner runner2 = new DownloadRunner(uri2); + runner1.postDownloadRequest(); + runner1.getDownloader(0).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + runner2.postDownloadRequest(); + + runner1.postRemoveAllRequest(); + runner1.getDownloader(1).unblock(); + runner2.getDownloader(1).unblock(); + downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + runner1.getTask().assertRemoved(); + runner2.getTask().assertRemoved(); + assertThat(downloadManager.getCurrentDownloads()).isEmpty(); + assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0); + } + @Test public void differentDownloadRequestsMerged() throws Throwable { DownloadRunner runner = new DownloadRunner(uri1); @@ -605,6 +626,11 @@ public class DownloadManagerTest { return this; } + private DownloadRunner postRemoveAllRequest() { + runOnMainThread(() -> downloadManager.removeAllDownloads()); + return this; + } + private DownloadRunner postDownloadRequest(StreamKey... keys) { DownloadRequest downloadRequest = new DownloadRequest( From 887116cd2db27388f2719f098cab54d50486f3e2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 04:40:24 +0100 Subject: [PATCH 009/807] Increase gapless trim sample count PiperOrigin-RevId: 247348352 --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 18ca81c72f..3b74240379 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 @@ -60,7 +60,7 @@ import java.util.List; * The threshold number of samples to trim from the start/end of an audio track when applying an * edit below which gapless info can be used (rather than removing samples from the sample table). */ - private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3; + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4; /** The magic signature for an Opus Identification header, as defined in RFC-7845. */ private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); From 3a3a941abdf68e03d67bfdc6f5703a00759de40d Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 May 2019 14:40:51 +0100 Subject: [PATCH 010/807] Ensure messages get deleted if they throw an exception. If a PlayerMessage throws an exception, it is currently not deleted from the list of pending messages. This may be problematic as the list of pending messages is kept when the player is retried without reset and the message is sent again in such a case. PiperOrigin-RevId: 247414494 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 742f300df1..34d8d0aa08 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 @@ -1052,11 +1052,14 @@ import java.util.concurrent.atomic.AtomicBoolean; && nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendMessageToTarget(nextInfo.message); - if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { - pendingMessages.remove(nextPendingMessageIndex); - } else { - nextPendingMessageIndex++; + try { + sendMessageToTarget(nextInfo.message); + } finally { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { + pendingMessages.remove(nextPendingMessageIndex); + } else { + nextPendingMessageIndex++; + } } nextInfo = nextPendingMessageIndex < pendingMessages.size() From 0a6f81a2ccd5b29ed20565dc8ec9630205062979 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 9 May 2019 15:10:41 +0100 Subject: [PATCH 011/807] Update player accessed on wrong thread URL PiperOrigin-RevId: 247418601 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2ac71db44f..056038d97a 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 @@ -1232,7 +1232,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/faqs.html#" + + "https://exoplayer.dev/troubleshooting.html#" + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; From 3b6058481356fc5dd639f189ffe95960ddd2c959 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 9 May 2019 16:16:08 +0100 Subject: [PATCH 012/807] Add a release callback to DefaultDrmSession In preparation for reference counting in DrmSession PiperOrigin-RevId: 247428114 --- .../exoplayer2/drm/DefaultDrmSession.java | 52 ++++++++------- .../drm/DefaultDrmSessionManager.java | 64 +++++++++---------- 2 files changed, 60 insertions(+), 56 deletions(-) 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 c2faaba823..215a48fc50 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 @@ -42,20 +42,16 @@ 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}. - */ +/** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { - /** - * Manages provisioning requests. - */ + /** Manages provisioning requests. */ public interface ProvisioningManager { /** - * Called when a session requires provisioning. The manager may call - * {@link #provision()} to have this session perform the provisioning operation. The manager + * Called when a session requires provisioning. The manager may call {@link + * #provision()} to have this session perform the provisioning operation. The manager * will call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails. * @@ -70,11 +66,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ void onProvisionError(Exception error); - /** - * Called by a session when it successfully completes a provisioning operation. - */ + /** Called by a session when it successfully completes a provisioning operation. */ void onProvisionCompleted(); + } + /** Callback to be notified when the session is released. */ + public interface ReleaseCallback { + + /** + * Called when the session is released. + * + * @param session The session. + */ + void onSessionReleased(DefaultDrmSession session); } private static final String TAG = "DefaultDrmSession"; @@ -88,6 +92,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; + private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; private final @Nullable HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; @@ -115,6 +120,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. * @param provisioningManager The manager for provisioning. + * @param releaseCallback The {@link ReleaseCallback}. * @param schemeDatas DRM scheme datas for this session, or null if an {@code * offlineLicenseKeySetId} is provided. * @param mode The DRM mode. @@ -131,6 +137,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, + ReleaseCallback releaseCallback, @Nullable List schemeDatas, @DefaultDrmSessionManager.Mode int mode, @Nullable byte[] offlineLicenseKeySetId, @@ -145,6 +152,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } this.uuid = uuid; this.provisioningManager = provisioningManager; + this.releaseCallback = releaseCallback; this.mediaDrm = mediaDrm; this.mode = mode; if (offlineLicenseKeySetId != null) { @@ -178,10 +186,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } - /** @return True if the session is closed and cleaned up, false otherwise. */ // Assigning null to various non-null variables for clean-up. Class won't be used after release. @SuppressWarnings("assignment.type.incompatible") - public boolean release() { + public void release() { if (--openCount == 0) { state = STATE_RELEASED; postResponseHandler.removeCallbacksAndMessages(null); @@ -198,9 +205,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sessionId = null; eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); } - return true; + releaseCallback.onSessionReleased(this); } - return false; } public boolean hasSessionId(byte[] sessionId) { @@ -330,8 +336,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); if (mode == DefaultDrmSessionManager.MODE_PLAYBACK && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { - Log.d(TAG, "Offline license has expired or will expire soon. " - + "Remaining seconds: " + licenseDurationRemainingSec); + Log.d( + TAG, + "Offline license has expired or will expire soon. " + + "Remaining seconds: " + + licenseDurationRemainingSec); postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); } else if (licenseDurationRemainingSec <= 0) { onError(new KeysExpiredException()); @@ -415,8 +424,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD - || (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null)) - && keySetId != null && keySetId.length != 0) { + || (mode == DefaultDrmSessionManager.MODE_PLAYBACK + && offlineLicenseKeySetId != null)) + && keySetId != null + && keySetId.length != 0) { offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; @@ -480,10 +491,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; break; default: break; - } } - } @SuppressLint("HandlerLeak") @@ -541,6 +550,5 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private long getRetryDelayMillis(int errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } - } } 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 3820836e49..f27fefa055 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 @@ -40,12 +40,10 @@ import java.util.HashMap; import java.util.List; import java.util.UUID; -/** - * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. - */ +/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager, - ProvisioningManager { +public class DefaultDrmSessionManager + implements DrmSessionManager, ProvisioningManager { /** * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does @@ -76,9 +74,7 @@ public class DefaultDrmSessionManager implements DrmSe * licenses. */ public static final int MODE_PLAYBACK = 0; - /** - * Restores an offline license to allow its status to be queried. - */ + /** Restores an offline license to allow its status to be queried. */ public static final int MODE_QUERY = 1; /** Downloads an offline license or renews an existing one. */ public static final int MODE_DOWNLOAD = 2; @@ -272,8 +268,8 @@ public class DefaultDrmSessionManager implements DrmSe /** * Provides access to {@link ExoMediaDrm#getPropertyString(String)}. - *

- * This method may be called when the manager is in any state. + * + *

This method may be called when the manager is in any state. * * @param key The key to request. * @return The retrieved property. @@ -284,8 +280,8 @@ public class DefaultDrmSessionManager implements DrmSe /** * Provides access to {@link ExoMediaDrm#setPropertyString(String, String)}. - *

- * This method may be called when the manager is in any state. + * + *

This method may be called when the manager is in any state. * * @param key The property to write. * @param value The value to write. @@ -296,8 +292,8 @@ public class DefaultDrmSessionManager implements DrmSe /** * Provides access to {@link ExoMediaDrm#getPropertyByteArray(String)}. - *

- * This method may be called when the manager is in any state. + * + *

This method may be called when the manager is in any state. * * @param key The key to request. * @return The retrieved property. @@ -308,8 +304,8 @@ public class DefaultDrmSessionManager implements DrmSe /** * Provides access to {@link ExoMediaDrm#setPropertyByteArray(String, byte[])}. - *

- * This method may be called when the manager is in any state. + * + *

This method may be called when the manager is in any state. * * @param key The property to write. * @param value The value to write. @@ -373,7 +369,8 @@ public class DefaultDrmSessionManager implements DrmSe if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { // If there is no scheme information, assume patternless AES-CTR. return true; - } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) + } else if (C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cbcs.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType)) { // API support for AES-CBC and pattern encryption was added in API 24. However, the // implementation was not stable until API 25. @@ -423,7 +420,8 @@ public class DefaultDrmSessionManager implements DrmSe new DefaultDrmSession<>( uuid, mediaDrm, - this, + /* provisioningManager= */ this, + /* releaseCallback= */ this::onSessionReleased, schemeDatas, mode, offlineLicenseKeySetId, @@ -444,17 +442,7 @@ public class DefaultDrmSessionManager implements DrmSe // Do nothing. return; } - - DefaultDrmSession drmSession = (DefaultDrmSession) session; - if (drmSession.release()) { - sessions.remove(drmSession); - if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { - // Other sessions were waiting for the released session to complete a provision operation. - // We need to have one of those sessions perform the provision operation instead. - provisioningSessions.get(1).provision(); - } - provisioningSessions.remove(drmSession); - } + ((DefaultDrmSession) session).release(); } // ProvisioningManager implementation. @@ -490,6 +478,16 @@ public class DefaultDrmSessionManager implements DrmSe // Internal methods. + private void onSessionReleased(DefaultDrmSession drmSession) { + sessions.remove(drmSession); + if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { + // Other sessions were waiting for the released session to complete a provision operation. + // We need to have one of those sessions perform the provision operation instead. + provisioningSessions.get(1).provision(); + } + provisioningSessions.remove(drmSession); + } + /** * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. * @@ -506,8 +504,9 @@ public class DefaultDrmSessionManager implements DrmSe List matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount); for (int i = 0; i < drmInitData.schemeDataCount; i++) { SchemeData schemeData = drmInitData.get(i); - boolean uuidMatches = schemeData.matches(uuid) - || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); + boolean uuidMatches = + schemeData.matches(uuid) + || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); if (uuidMatches && (schemeData.data != null || allowMissingData)) { matchingSchemeDatas.add(schemeData); } @@ -536,7 +535,6 @@ public class DefaultDrmSessionManager implements DrmSe } } } - } private class MediaDrmEventListener implements OnEventListener { @@ -550,7 +548,5 @@ public class DefaultDrmSessionManager implements DrmSe @Nullable byte[] data) { Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); } - } - } From 95495328cf93135f9d6ebf338ca46f57be6a2d84 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 9 May 2019 22:41:07 +0100 Subject: [PATCH 013/807] ...Androidx Migration... PiperOrigin-RevId: 247499141 --- .../google/android/exoplayer2/demo/TrackSelectionDialog.java | 2 +- demos/main/src/main/res/layout/track_selection_dialog.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 86d01706fb..d3e8b49b56 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 @@ -25,13 +25,13 @@ import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; import androidx.appcompat.app.AppCompatDialog; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import androidx.viewpager.widget.ViewPager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; diff --git a/demos/main/src/main/res/layout/track_selection_dialog.xml b/demos/main/src/main/res/layout/track_selection_dialog.xml index 24d101ae4c..7f6c45e131 100644 --- a/demos/main/src/main/res/layout/track_selection_dialog.xml +++ b/demos/main/src/main/res/layout/track_selection_dialog.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + Date: Fri, 10 May 2019 16:23:02 +0100 Subject: [PATCH 014/807] Fix NPE in HLS deriveAudioFormat. Issue:#5868 PiperOrigin-RevId: 247613811 --- RELEASENOTES.md | 4 +++- .../google/android/exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6094046b87..080f5a0510 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Fix NPE when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Offline: Add option to remove all downloads. * Decoders: * Prefer codecs that advertise format support over ones that do not, even if @@ -11,7 +13,7 @@ * Audio: fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). * UI: Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). ### 2.10.0 ### 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 ef233bb566..2cfd14c79d 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 @@ -802,7 +802,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; - roleFlags = mediaTagFormat.roleFlags; + roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; } From 23fa53bf0bfb08356f3efe538a8212fa0663adc2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 17:13:36 +0100 Subject: [PATCH 015/807] Add setCodecOperatingRate workaround for 48KHz audio on ZTE Axon7 mini. Issue:#5821 PiperOrigin-RevId: 247621164 --- RELEASENOTES.md | 4 +++- .../exoplayer2/audio/MediaCodecAudioRenderer.java | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 080f5a0510..84c933c52c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,7 +13,9 @@ * Audio: fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). * UI: Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). ### 2.10.0 ### 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 a2fe8244f3..2e1bd19f47 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 @@ -798,7 +798,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // Set codec configuration values. if (Util.SDK_INT >= 23) { mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); - if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) { mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } @@ -821,6 +821,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the device's decoders are known to not support setting the codec operating + * rate. + * + *

See GitHub issue #5821. + */ + private static boolean deviceDoesntSupportOperatingRate() { + return Util.SDK_INT == 23 + && ("ZTE B2017G".equals(Util.MODEL) || "AXON 7 mini".equals(Util.MODEL)); + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. From 90fc659b75e950483c6e42edc5c9a0df8fd99efd Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 May 2019 18:12:05 +0100 Subject: [PATCH 016/807] Fix Javadoc links. PiperOrigin-RevId: 247630389 --- .../exoplayer2/analytics/AnalyticsListener.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 9a0339f5d4..48578d8853 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 @@ -59,7 +59,7 @@ public interface AnalyticsListener { public final Timeline timeline; /** - * Window index in the {@code timeline} this event belongs to, or the prospective window index + * Window index in the {@link #timeline} this event belongs to, or the prospective window index * if the timeline is not yet known and empty. */ public final int windowIndex; @@ -76,7 +76,7 @@ public interface AnalyticsListener { public final long eventPlaybackPositionMs; /** - * Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the + * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the * currently playing ad at the time of the event, in milliseconds. */ public final long currentPlaybackPositionMs; @@ -91,15 +91,15 @@ public interface AnalyticsListener { * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at * the time of the event, in milliseconds. * @param timeline Timeline at the time of the event. - * @param windowIndex Window index in the {@code timeline} this event belongs to, or the + * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the * prospective window index if the timeline is not yet known and empty. * @param mediaPeriodId 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. * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time * of the event, in milliseconds. - * @param currentPlaybackPositionMs Position in the current timeline window ({@code - * timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event, - * in milliseconds. + * @param currentPlaybackPositionMs Position in the current timeline window ({@link + * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in + * milliseconds. * @param totalBufferedDurationMs Total buffered duration from {@link * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes * pre-buffered data for subsequent ads and windows. From 7d1ce3b13e2026c8429bb0dea9a7349866549365 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 10:52:36 +0100 Subject: [PATCH 017/807] Change legacy support UI dependency to more specific ViewPager dependency. PiperOrigin-RevId: 247902405 --- demos/main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 494af011a5..0bce1d4b82 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -63,7 +63,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' - implementation 'com.android.support:support-core-ui:' + supportLibraryVersion + implementation 'androidx.viewpager:viewpager:1.0.0' implementation 'androidx.fragment:fragment:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') From 43195ed752d0af93e243daa9cd5ea37cbf96f6ba Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 11:56:35 +0100 Subject: [PATCH 018/807] Increase gradle heap size The update to Gradle 5.1.1 decreased the default heap size to 512MB and our build runs into Out-of-Memory errors. Setting the gradle flags to higher values instead. See https://developer.android.com/studio/releases/gradle-plugin#3-4-0 PiperOrigin-RevId: 247908526 --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index 4b9bfa8fa2..31ff0ad6b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.enableUnitTestBinaryResources=true buildDir=buildout +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m From a849f43e6d8b64bbc816cdb93444b96e22ae4c28 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 13 May 2019 15:56:23 +0100 Subject: [PATCH 019/807] Work around broken raw audio decoder on Oppo R9 Issue: #5782 PiperOrigin-RevId: 247934223 --- RELEASENOTES.md | 2 ++ .../exoplayer2/mediacodec/MediaCodecInfo.java | 20 ++++++++++--------- .../exoplayer2/mediacodec/MediaCodecUtil.java | 10 ++++++++++ .../gts/EnumerateDecodersTest.java | 9 ++++++--- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 84c933c52c..6af3381a34 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,8 @@ ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). +* Add a workaround for broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). ### 2.10.0 ### 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 08ba94f257..e79c776f88 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 @@ -51,13 +51,13 @@ public final class MediaCodecInfo { public final String name; /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ - public final @Nullable String mimeType; + @Nullable public final String mimeType; /** - * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this - * is a passthrough codec. + * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not + * known. */ - public final @Nullable CodecCapabilities capabilities; + @Nullable public final CodecCapabilities capabilities; /** * Whether the decoder supports seamless resolution switches. @@ -109,11 +109,12 @@ 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. + * @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, - CodecCapabilities capabilities) { + public static MediaCodecInfo newInstance( + String name, String mimeType, @Nullable CodecCapabilities capabilities) { return new MediaCodecInfo( name, mimeType, @@ -128,7 +129,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. + * @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}. * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. @@ -136,7 +138,7 @@ public final class MediaCodecInfo { public static MediaCodecInfo newInstance( String name, String mimeType, - CodecCapabilities capabilities, + @Nullable CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( 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 f275dfe7d7..f3936e5dc2 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 @@ -504,6 +504,16 @@ public final class MediaCodecUtil { */ private static void applyWorkarounds(String mimeType, List decoderInfos) { if (MimeTypes.AUDIO_RAW.equals(mimeType)) { + if (Util.SDK_INT < 26 + && Util.DEVICE.equals("R9") + && decoderInfos.size() == 1 + && decoderInfos.get(0).name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This device does not list a generic raw audio decoder, yet it can be instantiated by + // name. See Issue #5782. + decoderInfos.add( + MediaCodecInfo.newInstance( + "OMX.google.raw.decoder", MimeTypes.AUDIO_RAW, /* capabilities= */ null)); + } // Work around inconsistent raw audio decoding behavior across different devices. sortByScore( decoderInfos, diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java index d256db8c30..e9d856015e 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java @@ -20,12 +20,12 @@ import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.testutil.MetricsLogger; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -93,14 +93,17 @@ public class EnumerateDecodersTest { List mediaCodecInfos = MediaCodecUtil.getDecoderInfos(mimeType, secure, tunneling); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfos) { - CodecCapabilities capabilities = Assertions.checkNotNull(mediaCodecInfo.capabilities); + CodecCapabilities capabilities = mediaCodecInfo.capabilities; metricsLogger.logMetric( "capabilities_" + mediaCodecInfo.name, codecCapabilitiesToString(mimeType, capabilities)); } } private static String codecCapabilitiesToString( - String requestedMimeType, CodecCapabilities codecCapabilities) { + String requestedMimeType, @Nullable CodecCapabilities codecCapabilities) { + if (codecCapabilities == null) { + return "[null]"; + } boolean isVideo = MimeTypes.isVideo(requestedMimeType); boolean isAudio = MimeTypes.isAudio(requestedMimeType); StringBuilder result = new StringBuilder(); From 0612d9f6d51621eafa5ce0e86edd873ce09bc943 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 May 2019 16:05:34 +0100 Subject: [PATCH 020/807] Allow line terminators in ICY metadata Issue: #5876 PiperOrigin-RevId: 247935822 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/metadata/icy/IcyDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoderTest.java | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6af3381a34..5ff8644ce4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Offline: Add option to remove all downloads. * Decoders: * Prefer codecs that advertise format support over ones that do not, even if diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index d04cd3a999..489719eda4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder { private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';"); + private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 9cbcea5814..97aac9995d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -70,6 +70,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isEqualTo("test_url"); } + @Test + public void decode_lineTerminatorInTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_notIcy() { IcyDecoder decoder = new IcyDecoder(); From 15688e3e6f9cc2d8f8f8944c2ae1eaf7b5237b9a Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 May 2019 16:21:33 +0100 Subject: [PATCH 021/807] Fix Javadoc generation. Accessing task providers (like javaCompileProvider) at sync time is not possible. That's why the source sets of all generateJavadoc tasks is empty. The set of source directories can also be accessed directly through the static sourceSets field. Combining these allows to statically provide the relevant source files to the javadoc task without needing to access the run-time task provider. PiperOrigin-RevId: 247938176 --- javadoc_library.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/javadoc_library.gradle b/javadoc_library.gradle index a818ea390e..74fcc3dd6c 100644 --- a/javadoc_library.gradle +++ b/javadoc_library.gradle @@ -18,10 +18,13 @@ android.libraryVariants.all { variant -> if (!name.equals("release")) { return; // Skip non-release builds. } + def allSourceDirs = variant.sourceSets.inject ([]) { + acc, val -> acc << val.javaDirectories + } task("generateJavadoc", type: Javadoc) { description = "Generates Javadoc for the ${javadocTitle}." title = "ExoPlayer ${javadocTitle}" - source = variant.javaCompileProvider.get().source + source = allSourceDirs options { links "http://docs.oracle.com/javase/7/docs/api/" linksOffline "https://developer.android.com/reference", From 50c9ae0efcbfe79df40c1d12b91c3f8def938062 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 13 May 2019 19:17:00 +0100 Subject: [PATCH 022/807] Assume that encrypted content requires secure decoders in renderer support checks Issue:#5568 PiperOrigin-RevId: 247973411 --- RELEASENOTES.md | 2 ++ .../audio/MediaCodecAudioRenderer.java | 20 ++----------- .../android/exoplayer2/drm/DrmInitData.java | 30 ++----------------- .../exoplayer2/drm/FrameworkMediaDrm.java | 3 +- .../video/MediaCodecVideoRenderer.java | 27 ++++++++--------- .../dash/manifest/DashManifestParser.java | 9 +----- 6 files changed, 21 insertions(+), 70 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5ff8644ce4..a067bb4cc9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ ([#5868](https://github.com/google/ExoPlayer/issues/5868)). * Fix handling of line terminators in SHOUTcast ICY metadata ([#5876](https://github.com/google/ExoPlayer/issues/5876)). +* Assume that encrypted content requires secure decoders in renderer support + checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Offline: Add option to remove all downloads. * Decoders: * Prefer codecs that advertise format support over ones that do not, even if 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 2e1bd19f47..c3ec759c2d 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 @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; @@ -283,25 +282,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // Assume the decoder outputs 16-bit PCM, unless the input is raw. return FORMAT_UNSUPPORTED_SUBTYPE; } - boolean requiresSecureDecryption = false; - DrmInitData drmInitData = format.drmInitData; - if (drmInitData != null) { - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; - } - } List decoderInfos = - getDecoderInfos(mediaCodecSelector, format, requiresSecureDecryption); + getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); if (decoderInfos.isEmpty()) { - return requiresSecureDecryption - && !mediaCodecSelector - .getDecoderInfos( - format.sampleMimeType, - /* requiresSecureDecoder= */ false, - /* requiresTunnelingDecoder= */ false) - .isEmpty() - ? FORMAT_UNSUPPORTED_DRM - : FORMAT_UNSUPPORTED_SUBTYPE; + return FORMAT_UNSUPPORTED_SUBTYPE; } if (!supportsFormatDrm) { return FORMAT_UNSUPPORTED_DRM; 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 89c5dd6650..3b05bd1e41 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 @@ -291,10 +291,6 @@ public final class DrmInitData implements Comparator, Parcelable { public final String mimeType; /** The initialization data. May be null for scheme support checks only. */ public final @Nullable byte[] data; - /** - * Whether secure decryption is required. - */ - public final boolean requiresSecureDecryption; /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is @@ -303,19 +299,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @param data See {@link #data}. */ public SchemeData(UUID uuid, String mimeType, @Nullable byte[] data) { - this(uuid, mimeType, data, false); - } - - /** - * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is - * universal (i.e. applies to all schemes). - * @param mimeType See {@link #mimeType}. - * @param data See {@link #data}. - * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. - */ - public SchemeData( - UUID uuid, String mimeType, @Nullable byte[] data, boolean requiresSecureDecryption) { - this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption); + this(uuid, /* licenseServerUrl= */ null, mimeType, data); } /** @@ -324,19 +308,13 @@ public final class DrmInitData implements Comparator, Parcelable { * @param licenseServerUrl See {@link #licenseServerUrl}. * @param mimeType See {@link #mimeType}. * @param data See {@link #data}. - * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. */ public SchemeData( - UUID uuid, - @Nullable String licenseServerUrl, - String mimeType, - @Nullable byte[] data, - boolean requiresSecureDecryption) { + UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) { this.uuid = Assertions.checkNotNull(uuid); this.licenseServerUrl = licenseServerUrl; this.mimeType = Assertions.checkNotNull(mimeType); this.data = data; - this.requiresSecureDecryption = requiresSecureDecryption; } /* package */ SchemeData(Parcel in) { @@ -344,7 +322,6 @@ public final class DrmInitData implements Comparator, Parcelable { licenseServerUrl = in.readString(); mimeType = Util.castNonNull(in.readString()); data = in.createByteArray(); - requiresSecureDecryption = in.readByte() != 0; } /** @@ -381,7 +358,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @return The new instance. */ public SchemeData copyWithData(@Nullable byte[] data) { - return new SchemeData(uuid, licenseServerUrl, mimeType, data, requiresSecureDecryption); + return new SchemeData(uuid, licenseServerUrl, mimeType, data); } @Override @@ -425,7 +402,6 @@ public final class DrmInitData implements Comparator, Parcelable { dest.writeString(licenseServerUrl); dest.writeString(mimeType); dest.writeByteArray(data); - dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); } public static final Parcelable.Creator CREATOR = 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 615aa0e7b1..848d9e146a 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 @@ -242,8 +242,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm decoderInfos = getDecoderInfos( mediaCodecSelector, format, requiresSecureDecryption, /* requiresTunnelingDecoder= */ false); + if (requiresSecureDecryption && decoderInfos.isEmpty()) { + // No secure decoders are available. Fall back to non-secure decoders. + decoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + /* requiresSecureDecoder= */ false, + /* requiresTunnelingDecoder= */ false); + } if (decoderInfos.isEmpty()) { - return requiresSecureDecryption - && !getDecoderInfos( - mediaCodecSelector, - format, - /* requiresSecureDecoder= */ false, - /* requiresTunnelingDecoder= */ false) - .isEmpty() - ? FORMAT_UNSUPPORTED_DRM - : FORMAT_UNSUPPORTED_SUBTYPE; + return FORMAT_UNSUPPORTED_SUBTYPE; } if (!supportsFormatDrm(drmSessionManager, drmInitData)) { return FORMAT_UNSUPPORTED_DRM; 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 0e3c6a8bda..64ec1adb43 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 @@ -397,7 +397,6 @@ public class DashManifestParser extends DefaultHandler String licenseServerUrl = null; byte[] data = null; UUID uuid = null; - boolean requiresSecureDecoder = false; String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); if (schemeIdUri != null) { @@ -431,9 +430,6 @@ public class DashManifestParser extends DefaultHandler xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); - } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { - String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); - requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } else if (data == null && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh") && xpp.next() == XmlPullParser.TEXT) { @@ -457,10 +453,7 @@ public class DashManifestParser extends DefaultHandler } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); SchemeData schemeData = - uuid != null - ? new SchemeData( - uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) - : null; + uuid != null ? new SchemeData(uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data) : null; return Pair.create(schemeType, schemeData); } From 14915fd1485f0d408a7ad5172fa506d40b9b9f2b Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 May 2019 12:33:19 +0100 Subject: [PATCH 023/807] Fix rendering DVB subtitle on API 28. Issue: #5862 PiperOrigin-RevId: 248112524 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/text/dvb/DvbParser.java | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a067bb4cc9..bcdef40479 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). +* Fix DVB subtitles for SDK 28 + ([#5862](https://github.com/google/ExoPlayer/issues/5862)). ### 2.10.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index eb956f06db..3f2fef454f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Region; import android.util.SparseArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; @@ -150,6 +149,8 @@ import java.util.List; List cues = new ArrayList<>(); SparseArray pageRegions = subtitleService.pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { + // Save clean clipping state. + canvas.save(); PageRegion pageRegion = pageRegions.valueAt(i); int regionId = pageRegions.keyAt(i); RegionComposition regionComposition = subtitleService.regions.get(regionId); @@ -163,9 +164,7 @@ import java.util.List; displayDefinition.horizontalPositionMaximum); int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, displayDefinition.verticalPositionMaximum); - canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, - Region.Op.REPLACE); - + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); if (clutDefinition == null) { clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); @@ -214,9 +213,11 @@ import java.util.List; (float) regionComposition.height / displayDefinition.height)); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Restore clean clipping state. + canvas.restore(); } - return cues; + return Collections.unmodifiableList(cues); } // Static parsing. From 9d450e52f28aaff7475dd3f0cb8ff59357c64c1a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 May 2019 13:42:16 +0100 Subject: [PATCH 024/807] Allow empty values in ICY metadata Issue: #5876 PiperOrigin-RevId: 248119726 --- RELEASENOTES.md | 2 +- .../android/exoplayer2/metadata/icy/IcyDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoderTest.java | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bcdef40479..a7c998a2ef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,7 +4,7 @@ * Fix NPE when using HLS chunkless preparation ([#5868](https://github.com/google/ExoPlayer/issues/5868)). -* Fix handling of line terminators in SHOUTcast ICY metadata +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 489719eda4..3d873926bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder { private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';", Pattern.DOTALL); + private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 97aac9995d..4602d172a6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -48,6 +48,17 @@ public final class IcyDecoderTest { assertThat(streamInfo.url).isNull(); } + @Test + public void decode_emptyTitle() { + IcyDecoder decoder = new IcyDecoder(); + Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.title).isEmpty(); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + @Test public void decode_semiColonInTitle() { IcyDecoder decoder = new IcyDecoder(); From cf389268b01d9616089e298e2d41a1e22ffd1408 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 14 May 2019 23:18:42 +0100 Subject: [PATCH 025/807] Add links to the developer guide in some READMEs PiperOrigin-RevId: 248221982 --- library/dash/README.md | 2 ++ library/hls/README.md | 2 ++ library/smoothstreaming/README.md | 2 ++ library/ui/README.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/library/dash/README.md b/library/dash/README.md index 7831033b99..1076716684 100644 --- a/library/dash/README.md +++ b/library/dash/README.md @@ -6,7 +6,9 @@ play DASH content, instantiate a `DashMediaSource` and pass it to ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/dash.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/hls/README.md b/library/hls/README.md index 1dd1b7a62e..3470c29e3c 100644 --- a/library/hls/README.md +++ b/library/hls/README.md @@ -5,7 +5,9 @@ instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/hls.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md index 4fa24543d6..d53471d17c 100644 --- a/library/smoothstreaming/README.md +++ b/library/smoothstreaming/README.md @@ -5,8 +5,10 @@ instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/smoothstreaming.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md index 341ea2fb16..16136b3d94 100644 --- a/library/ui/README.md +++ b/library/ui/README.md @@ -4,7 +4,9 @@ Provides UI components and resources for use with ExoPlayer. ## Links ## +* [Developer Guide][]. * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` belong to this module. +[Developer Guide]: https://exoplayer.dev/ui-components.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html From 8edce41ff3537a91075070b6e8e8e9f62a2cb1c8 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 May 2019 17:50:50 +0100 Subject: [PATCH 026/807] Add simpler HttpDataSource constructors PiperOrigin-RevId: 248350557 --- .../ext/cronet/CronetDataSource.java | 24 +++++++++++++++---- .../ext/okhttp/OkHttpDataSource.java | 9 +++++++ .../upstream/DefaultHttpDataSource.java | 5 ++++ .../exoplayer2/testutil/FakeMediaChunk.java | 2 +- 4 files changed, 34 insertions(+), 6 deletions(-) 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 a9995af0e4..ca196b1d2f 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 @@ -113,7 +113,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final CronetEngine cronetEngine; private final Executor executor; - private final Predicate contentTypePredicate; + @Nullable private final Predicate contentTypePredicate; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -146,6 +146,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; + /** + * @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. + */ + public CronetDataSource(CronetEngine cronetEngine, Executor executor) { + this(cronetEngine, executor, /* contentTypePredicate= */ null); + } + /** * @param cronetEngine A CronetEngine. * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may @@ -158,7 +170,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * #open(DataSpec)}. */ public CronetDataSource( - CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate) { + CronetEngine cronetEngine, + Executor executor, + @Nullable Predicate contentTypePredicate) { this( cronetEngine, executor, @@ -188,7 +202,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -225,7 +239,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -246,7 +260,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* package */ CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, 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 a749495184..8eb8bba920 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 @@ -73,6 +73,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { private long bytesSkipped; private long bytesRead; + /** + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. + * @param userAgent An optional User-Agent string. + */ + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { + this(callFactory, userAgent, /* contentTypePredicate= */ null); + } + /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. 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 6aad517004..66036b7a84 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 @@ -89,6 +89,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private long bytesSkipped; private long bytesRead; + /** @param userAgent The User-Agent string that should be used. */ + public DefaultHttpDataSource(String userAgent) { + this(userAgent, /* contentTypePredicate= */ 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 diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java index 6669504c07..fd7be241df 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java @@ -27,7 +27,7 @@ import java.io.IOException; /** Fake {@link MediaChunk}. */ public final class FakeMediaChunk extends MediaChunk { - private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); + private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT"); public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs); From 4ca670bed3534aa7c925a7575e6fb7cf70d7fc64 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 15 May 2019 19:13:34 +0100 Subject: [PATCH 027/807] Use MediaSourceFactory interface to simplify DownloadHelper PiperOrigin-RevId: 248367983 --- .../exoplayer2/imademo/PlayerManager.java | 5 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- library/core/proguard-rules.txt | 6 -- .../exoplayer2/offline/DownloadHelper.java | 98 +++++++++---------- .../source/ExtractorMediaSource.java | 3 +- .../exoplayer2/source/MediaSourceFactory.java | 51 ++++++++++ .../source/ProgressiveMediaSource.java | 13 ++- .../exoplayer2/source/ads/AdsMediaSource.java | 22 +---- .../source/dash/DashMediaSource.java | 52 +++++----- .../exoplayer2/source/hls/HlsMediaSource.java | 52 +++++----- .../source/smoothstreaming/SsMediaSource.java | 52 +++++----- 11 files changed, 179 insertions(+), 180 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 05c804c7a8..8f2c891e3a 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -35,7 +36,7 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; /** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ -/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { +/* package */ final class PlayerManager implements MediaSourceFactory { private final ImaAdsLoader adsLoader; private final DataSource.Factory dataSourceFactory; @@ -89,7 +90,7 @@ import com.google.android.exoplayer2.util.Util; adsLoader.release(); } - // AdsMediaSource.MediaSourceFactory implementation. + // MediaSourceFactory implementation. @Override public MediaSource createMediaSource(Uri uri) { 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 8f46400670..8ee9e9f9f6 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 @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -570,8 +571,8 @@ public class PlayerActivity extends AppCompatActivity adsLoader = loaderConstructor.newInstance(this, adTagUri); } adsLoader.setPlayer(player); - AdsMediaSource.MediaSourceFactory adMediaSourceFactory = - new AdsMediaSource.MediaSourceFactory() { + MediaSourceFactory adMediaSourceFactory = + new MediaSourceFactory() { @Override public MediaSource createMediaSource(Uri uri) { return PlayerActivity.this.buildMediaSource(uri); diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 8c11810506..1f7a8d0ee7 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -48,20 +48,14 @@ -dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory -keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); - ** setStreamKeys(java.util.List); - com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory -keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); - ** setStreamKeys(java.util.List); - com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri); } -dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory -keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { (com.google.android.exoplayer2.upstream.DataSource$Factory); - ** setStreamKeys(java.util.List); - com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri); } # Don't warn about checkerframework 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 755f7e0343..d2b7bd84d2 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 @@ -31,6 +31,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.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -51,7 +52,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -107,13 +107,17 @@ public final class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } - private static final MediaSourceFactory DASH_FACTORY = - getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); - private static final MediaSourceFactory SS_FACTORY = - getMediaSourceFactory( - "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); - private static final MediaSourceFactory HLS_FACTORY = - getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + @Nullable + private static final Constructor DASH_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + + @Nullable + private static final Constructor SS_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + + @Nullable + private static final Constructor HLS_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); /** * Creates a {@link DownloadHelper} for progressive streams. @@ -186,7 +190,8 @@ public final class DownloadHelper { DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, - DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), + createMediaSourceInternal( + DASH_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -235,7 +240,8 @@ public final class DownloadHelper { DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, - HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), + createMediaSourceInternal( + HLS_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -284,7 +290,8 @@ public final class DownloadHelper { DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, - SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), + createMediaSourceInternal( + SS_FACTORY_CONSTRUCTOR, uri, dataSourceFactory, /* streamKeys= */ null), trackSelectorParameters, Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } @@ -299,16 +306,16 @@ public final class DownloadHelper { */ public static MediaSource createMediaSource( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { - MediaSourceFactory factory; + Constructor constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: - factory = DASH_FACTORY; + constructor = DASH_FACTORY_CONSTRUCTOR; break; case DownloadRequest.TYPE_SS: - factory = SS_FACTORY; + constructor = SS_FACTORY_CONSTRUCTOR; break; case DownloadRequest.TYPE_HLS: - factory = HLS_FACTORY; + constructor = HLS_FACTORY_CONSTRUCTOR; break; case DownloadRequest.TYPE_PROGRESSIVE: return new ProgressiveMediaSource.Factory(dataSourceFactory) @@ -316,8 +323,8 @@ public final class DownloadHelper { default: throw new IllegalStateException("Unsupported type: " + downloadRequest.type); } - return factory.createMediaSource( - downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + return createMediaSourceInternal( + constructor, downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); } private final String downloadType; @@ -752,54 +759,39 @@ public final class DownloadHelper { } } - private static MediaSourceFactory getMediaSourceFactory(String className) { - Constructor constructor = null; - Method setStreamKeysMethod = null; - Method createMethod = null; + @Nullable + private static Constructor getConstructor(String className) { try { // LINT.IfChange - Class factoryClazz = Class.forName(className); - constructor = factoryClazz.getConstructor(Factory.class); - setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class); - createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); + Class factoryClazz = + Class.forName(className).asSubclass(MediaSourceFactory.class); + return factoryClazz.getConstructor(Factory.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the respective module. - } catch (NoSuchMethodException | SecurityException e) { + return null; + } catch (NoSuchMethodException e) { // Something is wrong with the library or the proguard configuration. throw new IllegalStateException(e); } - return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod); } - private static final class MediaSourceFactory { - @Nullable private final Constructor constructor; - @Nullable private final Method setStreamKeysMethod; - @Nullable private final Method createMethod; - - public MediaSourceFactory( - @Nullable Constructor constructor, - @Nullable Method setStreamKeysMethod, - @Nullable Method createMethod) { - this.constructor = constructor; - this.setStreamKeysMethod = setStreamKeysMethod; - this.createMethod = createMethod; + private static MediaSource createMediaSourceInternal( + @Nullable Constructor constructor, + Uri uri, + Factory dataSourceFactory, + @Nullable List streamKeys) { + if (constructor == null) { + throw new IllegalStateException("Module missing to create media source."); } - - private MediaSource createMediaSource( - Uri uri, Factory dataSourceFactory, @Nullable List streamKeys) { - if (constructor == null || setStreamKeysMethod == null || createMethod == null) { - throw new IllegalStateException("Module missing to create media source."); - } - try { - Object factory = constructor.newInstance(dataSourceFactory); - if (streamKeys != null) { - setStreamKeysMethod.invoke(factory, streamKeys); - } - return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri)); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate media source.", e); + try { + MediaSourceFactory factory = constructor.newInstance(dataSourceFactory); + if (streamKeys != null) { + factory.setStreamKeys(streamKeys); } + return Assertions.checkNotNull(factory.createMediaSource(uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); } } 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 f3ed19db3d..3951dc20a2 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 @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; @@ -61,7 +60,7 @@ public final class ExtractorMediaSource extends BaseMediaSource /** Use {@link ProgressiveMediaSource.Factory} instead. */ @Deprecated - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java new file mode 100644 index 0000000000..c53abd1235 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -0,0 +1,51 @@ +/* + * 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 android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.StreamKey; +import java.util.List; + +/** Factory for creating {@link MediaSource}s from URIs. */ +public interface MediaSourceFactory { + + /** + * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered. + * + * @param streamKeys A list of {@link StreamKey StreamKeys}. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + default MediaSourceFactory setStreamKeys(List streamKeys) { + return this; + } + + /** + * Creates a new {@link MediaSource} with the specified {@code uri}. + * + * @param uri The URI to play. + * @return The new {@link MediaSource media source}. + */ + MediaSource createMediaSource(Uri uri); + + /** + * Returns the {@link C.ContentType content types} supported by media sources created by this + * factory. + */ + @C.ContentType + int[] getSupportedTypes(); +} 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 f448b0b6c8..5ed12154b3 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 @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; @@ -45,7 +44,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource implements ProgressiveMediaPeriod.Listener { /** Factory for {@link ProgressiveMediaSource}s. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; @@ -87,7 +86,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * possible formats are known, pass a factory that instantiates extractors for those * formats. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. * @deprecated Pass the {@link ExtractorsFactory} via {@link #Factory(DataSource.Factory, * ExtractorsFactory)}. This is necessary so that proguard can treat the default extractors * factory as unused. @@ -106,7 +105,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * @param customCacheKey A custom key that uniquely identifies the original stream. Used for * cache indexing. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setCustomCacheKey(String customCacheKey) { Assertions.checkState(!isCreateCalled); @@ -121,7 +120,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * * @param tag A tag for the media source. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setTag(Object tag) { Assertions.checkState(!isCreateCalled); @@ -135,7 +134,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkState(!isCreateCalled); @@ -152,7 +151,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * each invocation of {@link * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { Assertions.checkState(!isCreateCalled); 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 1998977961..8828e34304 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 @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -54,27 +55,6 @@ import java.util.Map; */ public final class AdsMediaSource extends CompositeMediaSource { - /** Factory for creating {@link MediaSource}s to play ad media. */ - public interface MediaSourceFactory { - - /** - * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. - * - * @param uri The URI of the media or manifest to play. - * @return The new media source. - */ - MediaSource createMediaSource(Uri uri); - - /** - * Returns the content types supported by media sources created by this factory. Each element - * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link - * C#TYPE_OTHER}. - * - * @return The content types supported by media sources created by this factory. - */ - int[] getSupportedTypes(); - } - /** * Wrapper for exceptions that occur while loading ads, which are notified via {@link * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, 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 cdc32553f3..709fd00ea7 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 @@ -34,8 +34,8 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; @@ -74,7 +74,7 @@ public final class DashMediaSource extends BaseMediaSource { } /** Factory for {@link DashMediaSource}s. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; @@ -213,19 +213,6 @@ public final class DashMediaSource extends BaseMediaSource { return this; } - /** - * Sets a list of {@link StreamKey stream keys} by which the manifest is filtered. - * - * @param streamKeys A list of {@link StreamKey stream keys}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; - } - /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc...). The default is an instance of {@link @@ -288,6 +275,22 @@ public final class DashMediaSource extends BaseMediaSource { return mediaSource; } + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + DashMediaSource mediaSource = createMediaSource(manifestUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + /** * Returns a new {@link DashMediaSource} using the current parameters. * @@ -316,20 +319,11 @@ public final class DashMediaSource extends BaseMediaSource { tag); } - /** - * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, - * MediaSourceEventListener)} instead. - */ - @Deprecated - public DashMediaSource createMediaSource( - Uri manifestUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { - DashMediaSource mediaSource = createMediaSource(manifestUri); - if (eventHandler != null && eventListener != null) { - mediaSource.addEventListener(eventHandler, eventListener); - } - return mediaSource; + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; } @Override 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 b6b874b293..be4484aa78 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 @@ -29,9 +29,9 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory; @@ -56,7 +56,7 @@ public final class HlsMediaSource extends BaseMediaSource } /** Factory for {@link HlsMediaSource}s. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final HlsDataSourceFactory hlsDataSourceFactory; @@ -177,19 +177,6 @@ public final class HlsMediaSource extends BaseMediaSource return this; } - /** - * Sets a list of {@link StreamKey stream keys} by which the playlists are filtered. - * - * @param streamKeys A list of {@link StreamKey stream keys}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; - } - /** * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link * DefaultHlsPlaylistTracker#FACTORY}. @@ -251,6 +238,22 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public HlsMediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + HlsMediaSource mediaSource = createMediaSource(playlistUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + /** * Returns a new {@link HlsMediaSource} using the current parameters. * @@ -276,20 +279,11 @@ public final class HlsMediaSource extends BaseMediaSource tag); } - /** - * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, - * MediaSourceEventListener)} instead. - */ - @Deprecated - public HlsMediaSource createMediaSource( - Uri playlistUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { - HlsMediaSource mediaSource = createMediaSource(playlistUri); - if (eventHandler != null && eventListener != null) { - mediaSource.addEventListener(eventHandler, eventListener); - } - return mediaSource; + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; } @Override 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 09820f092d..7b9f3e3c4f 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 @@ -31,9 +31,9 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; @@ -61,7 +61,7 @@ public final class SsMediaSource extends BaseMediaSource } /** Factory for {@link SsMediaSource}. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final SsChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; @@ -180,19 +180,6 @@ public final class SsMediaSource extends BaseMediaSource return this; } - /** - * Sets a list of {@link StreamKey stream keys} by which the manifest is filtered. - * - * @param streamKeys A list of {@link StreamKey stream keys}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; - } - /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc.). The default is an instance of {@link @@ -254,6 +241,22 @@ public final class SsMediaSource extends BaseMediaSource return mediaSource; } + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + SsMediaSource mediaSource = createMediaSource(manifestUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + /** * Returns a new {@link SsMediaSource} using the current parameters. * @@ -281,20 +284,11 @@ public final class SsMediaSource extends BaseMediaSource tag); } - /** - * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, - * MediaSourceEventListener)} instead. - */ - @Deprecated - public SsMediaSource createMediaSource( - Uri manifestUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { - SsMediaSource mediaSource = createMediaSource(manifestUri); - if (eventHandler != null && eventListener != null) { - mediaSource.addEventListener(eventHandler, eventListener); - } - return mediaSource; + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; } @Override From 59b2dd2701eba68ff6247ac23e9bb147933c47f0 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 May 2019 19:20:27 +0100 Subject: [PATCH 028/807] don't call stop before preparing the player Issue: #5891 PiperOrigin-RevId: 248369509 --- .../mediasession/MediaSessionConnector.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) 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 1330bd066e..afe53099ee 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 @@ -834,10 +834,9 @@ public final class MediaSessionConnector { return player != null && mediaButtonEventHandler != null; } - private void stopPlayerForPrepare(boolean playWhenReady) { + private void setPlayWhenReady(boolean playWhenReady) { if (player != null) { - player.stop(); - player.setPlayWhenReady(playWhenReady); + controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); } } @@ -1051,14 +1050,14 @@ public final class MediaSessionConnector { } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); } - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); } } @@ -1181,7 +1180,7 @@ public final class MediaSessionConnector { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1189,7 +1188,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1197,7 +1196,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1205,7 +1204,7 @@ public final class MediaSessionConnector { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -1213,7 +1212,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -1221,7 +1220,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromSearch(String query, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -1229,7 +1228,7 @@ public final class MediaSessionConnector { @Override public void onPlayFromUri(Uri uri, Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { - stopPlayerForPrepare(/* playWhenReady= */ true); + setPlayWhenReady(/* playWhenReady= */ true); playbackPreparer.onPrepareFromUri(uri, extras); } } From 7f79db072437fe8a221b53c05a7431996090f769 Mon Sep 17 00:00:00 2001 From: Adam Richter Date: Wed, 15 May 2019 13:44:26 -0700 Subject: [PATCH 029/807] Split a few assertThat(a && b).isTrue() calls into separate assertions for more precise diagnostics. --- .../com/google/android/exoplayer2/source/ShuffleOrderTest.java | 3 ++- .../android/exoplayer2/upstream/DataSourceInputStreamTest.java | 3 ++- .../google/android/exoplayer2/testutil/FakeExtractorInput.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) 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/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/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..20f6f436b0 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 @@ -86,7 +86,8 @@ public final class FakeExtractorInput implements ExtractorInput { * @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; } From 819d589b226d6e77a4f4bfd5ed8039e149b75cd4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 16 May 2019 11:42:05 +0100 Subject: [PATCH 030/807] Ignore empty timelines in ImaAdsLoader. We previously only checked whether the reason for the timeline change is RESET which indicates an empty timeline. Change this to an explicit check for empty timelines to also ignore empty media or intermittent timeline changes to an empty timeline which are not marked as RESET. Issue:#5831 PiperOrigin-RevId: 248499118 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e860d07a14..bdeebec44c 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 @@ -948,8 +948,8 @@ public final class ImaAdsLoader @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { - if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { - // The player is being reset and this source will be released. + if (timeline.isEmpty()) { + // The player is being reset or contains no media. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); From b5a512b6737408f6450cddefbd53308d5f047456 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 May 2019 12:30:13 +0100 Subject: [PATCH 031/807] Bump release to 2.10.1 and update release notes PiperOrigin-RevId: 248503235 --- RELEASENOTES.md | 26 ++++++++++--------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a7c998a2ef..ab42c4cc58 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,28 +2,30 @@ ### dev-v2 (not yet released) ### -* Fix NPE when using HLS chunkless preparation - ([#5868](https://github.com/google/ExoPlayer/issues/5868)). -* Fix handling of empty values and line terminators in SHOUTcast ICY metadata - ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). -* Offline: Add option to remove all downloads. -* Decoders: - * Prefer codecs that advertise format support over ones that do not, even if - they are listed lower in the `MediaCodecList`. - * CEA-608: Handle XDS and TEXT modes - ([5807](https://github.com/google/ExoPlayer/pull/5807)). +* Decoders: Prefer decoders that advertise format support over ones that do not, + even if they are listed lower in the `MediaCodecList`. +* CEA-608: Handle XDS and TEXT modes + ([5807](https://github.com/google/ExoPlayer/pull/5807)). * Audio: fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). * UI: Change playback controls toggle from touch down to touch up events ([#5784](https://github.com/google/ExoPlayer/issues/5784)). -* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing - 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). + +### 2.10.1 ### + +* Offline: Add option to remove all downloads. +* HLS: Fix `NullPointerException` when using HLS chunkless preparation + ([#5868](https://github.com/google/ExoPlayer/issues/5868)). +* Fix handling of empty values and line terminators in SHOUTcast ICY metadata + ([#5876](https://github.com/google/ExoPlayer/issues/5876)). * Fix DVB subtitles for SDK 28 ([#5862](https://github.com/google/ExoPlayer/issues/5862)). +* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing + 48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)). ### 2.10.0 ### diff --git a/constants.gradle b/constants.gradle index 0e668d2464..6e4cd58d09 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.0' - releaseVersionCode = 2010000 + releaseVersion = '2.10.1' + releaseVersionCode = 2010001 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 72760db31b..a90435227b 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.0"; + public static final String VERSION = "2.10.1"; /** 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.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1"; /** * 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 = 2010000; + public static final int VERSION_INT = 2010001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 835d1f3afedbddab6b5049c49834bf553ec51ff8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 May 2019 12:38:07 +0100 Subject: [PATCH 032/807] Fix platform scheduler javadoc PiperOrigin-RevId: 248503971 --- .../google/android/exoplayer2/scheduler/PlatformScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8572c9c7ca..e6679e1a5a 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 @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.util.Util; * * * - * * } From 2091aa5cf971055857209c10ec7f9f12b58f419f Mon Sep 17 00:00:00 2001 From: sr1990 Date: Sat, 18 May 2019 19:41:30 -0700 Subject: [PATCH 033/807] Support signalling of last segment number via supplemental descriptor in mpd --- .../source/dash/DefaultDashChunkSource.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 057f0262d0..2877b2a1cc 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 @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.Descriptor; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -325,10 +326,34 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } + List listDescriptors; + Integer lastSegmentNumberSchemeIdUri = Integer.MAX_VALUE; + String sampleMimeType = trackSelection.getFormat(periodIndex).sampleMimeType; + + if (sampleMimeType.contains("video") || sampleMimeType.contains("audio")) { + + int track_type = sampleMimeType.contains("video")? C.TRACK_TYPE_VIDEO : C.TRACK_TYPE_AUDIO; + + if (!manifest.getPeriod(periodIndex).adaptationSets.get(manifest.getPeriod(periodIndex) + .getAdaptationSetIndex(track_type)).supplementalProperties.isEmpty()) { + listDescriptors = manifest.getPeriod(periodIndex).adaptationSets + .get(manifest.getPeriod(periodIndex).getAdaptationSetIndex(track_type)) + .supplementalProperties; + for ( Descriptor descriptor: listDescriptors ) { + if (descriptor.schemeIdUri.equalsIgnoreCase + ("http://dashif.org/guidelines/last-segment-number")) { + lastSegmentNumberSchemeIdUri = Integer.valueOf(descriptor.value); + } + } + } + } + long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); long lastAvailableSegmentNum = - representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); + Math.min(representationHolder. + getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs), + lastSegmentNumberSchemeIdUri); updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); From 128ded5ba0210fc57c7b66f91d72c03708f0cb0e Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 16 May 2019 17:29:32 +0100 Subject: [PATCH 034/807] add playWhenReady to prepareXyz methods of PlaybackPreparer. Issue: #5891 PiperOrigin-RevId: 248541827 --- RELEASENOTES.md | 4 + .../mediasession/MediaSessionConnector.java | 73 +++++++++++-------- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index df386781ba..86b3ab32e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,10 @@ * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: Add Scheduler implementation which uses WorkManager. +* Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods + to indicate whether a controller sent a play or only a prepare command. This + allows to take advantage of decoder reuse with the MediaSessionConnector + ([#5891](https://github.com/google/ExoPlayer/issues/5891)). ### 2.10.1 ### 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..d03e8fbdbf 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 @@ -172,7 +172,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 +197,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); } /** @@ -834,12 +856,6 @@ public final class MediaSessionConnector { 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); @@ -1045,19 +1061,19 @@ 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); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } - setPlayWhenReady(/* playWhenReady= */ true); } } @Override public void onPause() { if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) { - setPlayWhenReady(/* playWhenReady= */ false); + controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); } } @@ -1180,56 +1196,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); } } From bfeec25b2a4579e18f0d9ffe02ec2ad273790638 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 17 May 2019 18:34:07 +0100 Subject: [PATCH 035/807] Add SilenceMediaSource Issue: #5735 PiperOrigin-RevId: 248745617 --- RELEASENOTES.md | 7 +- .../exoplayer2/source/SilenceMediaSource.java | 242 ++++++++++++++++++ 2 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 86b3ab32e1..a02bd629e5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,8 +8,11 @@ even if they are listed lower in the `MediaCodecList`. * CEA-608: Handle XDS and TEXT modes ([5807](https://github.com/google/ExoPlayer/pull/5807)). -* Audio: fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). * UI: Change playback controls toggle from touch down to touch up events ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Add a workaround for broken raw audio decoding on Oppo R9 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..b03dd0ea7c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -0,0 +1,242 @@ +/* + * 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 + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + refreshSourceInfo( + new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), + /* manifest= */ null); + } + + @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 + public 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) { + 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) { + 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 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 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 = getAudioByteCount(positionUs); + } + + @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; + } +} From 33c677846aaa221ad79b5f479695e05d880eb5ff Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 May 2019 17:08:19 +0100 Subject: [PATCH 036/807] Use versioned manifest in all Robolectric tests. We are currently defaulting to targetSdk=1 as no targetSdk is specified. Only tests which explicitly ask for another SDK use another test SDK. With the versioned manifest, all tests run using the targetSDK by default. PiperOrigin-RevId: 249060796 --- extensions/cast/src/test/AndroidManifest.xml | 4 +++- extensions/cronet/src/test/AndroidManifest.xml | 4 +++- extensions/ffmpeg/src/test/AndroidManifest.xml | 4 +++- extensions/flac/src/test/AndroidManifest.xml | 4 +++- extensions/ima/src/test/AndroidManifest.xml | 4 +++- extensions/opus/src/test/AndroidManifest.xml | 4 +++- extensions/rtmp/src/test/AndroidManifest.xml | 4 +++- extensions/vp9/src/test/AndroidManifest.xml | 4 +++- library/core/src/test/AndroidManifest.xml | 4 +++- library/dash/src/test/AndroidManifest.xml | 4 +++- library/hls/src/test/AndroidManifest.xml | 4 +++- library/smoothstreaming/src/test/AndroidManifest.xml | 4 +++- library/ui/src/test/AndroidManifest.xml | 4 +++- testutils/src/test/AndroidManifest.xml | 4 +++- 14 files changed, 42 insertions(+), 14 deletions(-) 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/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/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/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/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/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/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/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/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/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/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/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/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/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. --> - + + + From 07c4569b5f05e3737db90d216b40cb0ac6beab8f Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 May 2019 17:28:26 +0100 Subject: [PATCH 037/807] Limit tests with specific SDK level to max=TARGET_SDK. The unspecified default is the highest available SDK which may be larger than TARGET_SDK (as specified by the manifest). PiperOrigin-RevId: 249064173 --- .../android/exoplayer2/audio/AudioFocusManagerTest.java | 9 +++++---- .../android/exoplayer2/audio/DefaultAudioSinkTest.java | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) 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)) From a4586355402075736f1b2ac91aea7ca11f257537 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 20 May 2019 17:48:15 +0100 Subject: [PATCH 038/807] Add ProgressUpdateListener Issue: #5834 PiperOrigin-RevId: 249067445 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ui/PlayerControlView.java | 27 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a02bd629e5..bfe871b4cc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). +* Add ProgressUpdateListener to PlayerControlView + ([#5834](https://github.com/google/ExoPlayer/issues/5834)). ### 2.10.1 ### 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..54a592ce6f 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 @@ -188,6 +188,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 +247,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; @@ -454,6 +467,15 @@ public class PlayerControlView extends FrameLayout { 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}. * @@ -855,6 +877,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); From f3f885c6aafb84c3f6fcd7978892b09b45b21266 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 May 2019 17:53:08 +0100 Subject: [PATCH 039/807] Update a reference to SimpleExoPlayerView PiperOrigin-RevId: 249068395 --- library/ui/src/main/res/values/attrs.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index f4a7976ebd..27e6a5b3b8 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -24,7 +24,7 @@ - + From 468296c2bcdcd20fc1181a2957fa88542c7a2962 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 May 2019 11:00:54 +0100 Subject: [PATCH 040/807] Suppress remaining ConstantCaseForConstant warnings PiperOrigin-RevId: 249217126 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 17 ++++++++++++++++- .../extractor/mp4/FragmentedMp4Extractor.java | 3 ++- .../text/webvtt/Mp4WebvttDecoder.java | 10 +++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) 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..90f8b125ac 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,18 +42,33 @@ 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"; + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_soun = Util.getIntegerCodeForString("soun"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_text = Util.getIntegerCodeForString("text"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta"); /** 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..4d51fb9b8e 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 @@ -55,6 +55,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,7 +105,7 @@ public class FragmentedMp4Extractor implements Extractor { private static final String TAG = "FragmentedMp4Extractor"; - @SuppressWarnings("ConstantField") + @SuppressWarnings("ConstantCaseForConstants") private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = 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..5e425cc12f 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 @@ -24,15 +24,19 @@ 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; + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg"); + + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); private final ParsableByteArray sampleData; From 9aeaf2dbb098e58c9350b6f9014d53ce98b6d6b1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 May 2019 13:50:28 +0100 Subject: [PATCH 041/807] Add ResolvingDataSource for just-in-time resolution of DataSpecs. Issue:#5779 PiperOrigin-RevId: 249234058 --- RELEASENOTES.md | 2 + .../upstream/ResolvingDataSource.java | 134 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfe871b4cc..b0544c1d1b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, 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(); + } + } +} From 491edd1edc8b1d681bf008e83ab37c654099802b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 May 2019 15:02:16 +0100 Subject: [PATCH 042/807] Update surface directly from SphericalSurfaceView The SurfaceListener just sets the surface on the VideoComponent, but SphericalSurfaceView already accesses the VideoComponent directly so it seems simpler to update the surface directly. PiperOrigin-RevId: 249242185 --- .../android/exoplayer2/ui/PlayerView.java | 16 ---------- .../ui/spherical/SphericalSurfaceView.java | 32 +++---------------- 2 files changed, 4 insertions(+), 44 deletions(-) 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..21467c3e25 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,7 +48,6 @@ 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.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -405,7 +403,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; @@ -1368,7 +1365,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider TextOutput, VideoListener, OnLayoutChangeListener, - SphericalSurfaceView.SurfaceListener, SingleTapListener { // TextOutput implementation @@ -1458,18 +1454,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/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index f7b208d085..8f82ae17db 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; @@ -83,7 +69,6 @@ public final class SphericalSurfaceView extends GLSurfaceView { 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; @@ -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); }); From b6d6d8c41148cb32073cf3e32e5abd151243508f Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 21 May 2019 16:01:24 +0100 Subject: [PATCH 043/807] Deprecate JobDispatcherScheduler PiperOrigin-RevId: 249250184 --- extensions/jobdispatcher/README.md | 4 ++++ .../exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index f70125ba38..bd76868625 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`]. + 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 ## 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; From 37fc1d879d6728debd2685ea21167704c7dbcfb1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 16:05:56 +0100 Subject: [PATCH 044/807] Propagate attributes to DefaultTimeBar Issue: #5765 PiperOrigin-RevId: 249251150 --- RELEASENOTES.md | 7 +- .../android/exoplayer2/ui/DefaultTimeBar.java | 27 ++++-- .../exoplayer2/ui/PlayerControlView.java | 32 ++++++- .../android/exoplayer2/ui/PlayerView.java | 17 ++-- .../res/layout/exo_playback_control_view.xml | 3 +- library/ui/src/main/res/values/attrs.xml | 88 ++++++++++++++----- library/ui/src/main/res/values/ids.xml | 1 + 7 files changed, 137 insertions(+), 38 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b0544c1d1b..ed9635a340 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,8 +15,11 @@ for the underlying track was changing (e.g., at some period transitions). * Add `SilenceMediaSource` that can be used to play silence of a given duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). -* UI: Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: Add Scheduler implementation which uses WorkManager. 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..5c70203788 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 @@ -220,11 +220,26 @@ public class DefaultTimeBar extends View implements TimeBar { private @Nullable long[] adGroupTimesMs; private @Nullable boolean[] playedAdGroups; - /** Creates a new time bar. */ + 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); + } + // 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 54a592ce6f..b9b2456722 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} *
    @@ -330,9 +342,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); } 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 21467c3e25..5bb8324780 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 @@ -162,9 +162,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

    @@ -214,9 +215,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} *
    @@ -456,8 +458,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); 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 27e6a5b3b8..706fba0e0b 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -31,18 +31,36 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -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 @@ + From a727acd29280969a7c11b3ee5db73257a11e5970 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 18:14:38 +0100 Subject: [PATCH 045/807] Remove nullness test blacklist for RTMP extension PiperOrigin-RevId: 249274122 --- .../android/exoplayer2/ext/rtmp/RtmpDataSource.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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..bf5b8e9261 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; @@ -34,8 +36,8 @@ 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); @@ -66,7 +68,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 +89,7 @@ public final class RtmpDataSource extends BaseDataSource { } @Override + @Nullable public Uri getUri() { return uri; } From 52888ab55b6bc8c8ebe8f6723937efe27c9a6a4d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 18:21:42 +0100 Subject: [PATCH 046/807] Remove CronetEngineWrapper from nullness test blacklist PiperOrigin-RevId: 249275623 --- .../exoplayer2/ext/cronet/CronetEngineWrapper.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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. From 3efe3205354125d1d28bd41932278a8100a841cd Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 18:23:51 +0100 Subject: [PATCH 047/807] Remove deprecated DataSource constructors PiperOrigin-RevId: 249276112 --- .../exoplayer2/ext/rtmp/RtmpDataSource.java | 14 --- .../exoplayer2/upstream/AssetDataSource.java | 14 --- .../upstream/ContentDataSource.java | 14 --- .../upstream/DefaultDataSource.java | 85 +----------------- .../upstream/DefaultDataSourceFactory.java | 4 +- .../upstream/DefaultHttpDataSource.java | 87 +------------------ .../exoplayer2/upstream/FileDataSource.java | 12 --- .../upstream/RawResourceDataSource.java | 14 --- .../exoplayer2/upstream/UdpDataSource.java | 44 ---------- 9 files changed, 7 insertions(+), 281 deletions(-) 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 bf5b8e9261..366d1c120d 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 @@ -18,13 +18,11 @@ 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; 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; @@ -43,18 +41,6 @@ public final class RtmpDataSource extends BaseDataSource { 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); 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..eeb0f1c957 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 @@ -51,20 +51,6 @@ 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 { 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..8df69ffb7a 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 @@ -57,20 +57,6 @@ 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 { 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..aa13baa03e 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 @@ -43,9 +43,9 @@ import java.util.Map; * explicit dependency on ExoPlayer's RTMP extension. *
  • data: For parsing data inlined in the URI as defined in RFC 2397. *
  • 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 { @@ -131,85 +131,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); 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..a5dfad72f0 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 @@ -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..65b65efe2c 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 @@ -47,8 +47,8 @@ import java.util.regex.Pattern; * *

    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 { @@ -164,89 +164,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou this.defaultRequestProperties = defaultRequestProperties; } - /** - * @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)}. - */ - @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); - } - } - @Override public @Nullable Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); 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..cead366360 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 @@ -45,18 +45,6 @@ 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 { 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..7b70bcc5c4 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 @@ -78,20 +78,6 @@ 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 { 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..fcfeef3fb4 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 @@ -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; From 8669d6dc102f7a8c99e8fed6f5d29b973fa8503a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 20:04:42 +0100 Subject: [PATCH 048/807] Fix missing import PiperOrigin-RevId: 249298093 --- .../com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java | 1 + 1 file changed, 1 insertion(+) 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 366d1c120d..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 @@ -18,6 +18,7 @@ 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; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.BaseDataSource; From 21be28431840f8adf73ea1cbed3e2c7fa7cf86c7 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 May 2019 22:36:17 +0100 Subject: [PATCH 049/807] Replace runtime lookups of string integer codes Make these values compile-time constants, which can be inlined. PiperOrigin-RevId: 249327464 --- .../android/exoplayer2/audio/WavUtil.java | 8 +- .../extractor/flv/FlvExtractor.java | 3 +- .../extractor/mp3/Mp3Extractor.java | 7 +- .../exoplayer2/extractor/mp4/Atom.java | 221 +++++++++--------- .../exoplayer2/extractor/mp4/AtomParsers.java | 16 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../extractor/mp4/MetadataUtil.java | 59 +++-- .../extractor/mp4/Mp4Extractor.java | 3 +- .../exoplayer2/extractor/mp4/Sniffer.java | 55 +++-- .../extractor/ogg/OggPageHeader.java | 3 +- .../exoplayer2/extractor/ogg/OpusReader.java | 3 +- .../extractor/rawcc/RawCcExtractor.java | 3 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 3 +- .../exoplayer2/extractor/ts/Ac4Extractor.java | 3 +- .../extractor/ts/AdtsExtractor.java | 3 +- .../exoplayer2/extractor/ts/TsExtractor.java | 8 +- .../extractor/wav/WavHeaderReader.java | 7 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 6 +- .../android/exoplayer2/text/cea/CeaUtil.java | 3 +- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 4 +- .../text/webvtt/Mp4WebvttDecoder.java | 6 +- .../video/spherical/ProjectionDecoder.java | 12 +- 22 files changed, 212 insertions(+), 226 deletions(-) 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/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 0a2c0c46f6..15b36157fb 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; 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..8f13cfaa11 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; 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..9bfe383169 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; 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 90f8b125ac..ea45374f86 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 @@ -48,28 +48,28 @@ import java.util.List; private static final String TAG = "AtomParsers"; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); + private static final int TYPE_vide = 0x76696465; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_soun = Util.getIntegerCodeForString("soun"); + private static final int TYPE_soun = 0x736f756e; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_text = Util.getIntegerCodeForString("text"); + private static final int TYPE_text = 0x74657874; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); + private static final int TYPE_sbtl = 0x7362746c; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); + private static final int TYPE_subt = 0x73756274; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); + private static final int TYPE_clcp = 0x636c6370; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + private static final int TYPE_meta = 0x6d657461; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta"); + 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 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 4d51fb9b8e..e0673dd4fa 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 @@ -106,7 +106,7 @@ public class FragmentedMp4Extractor implements Extractor { private static final String TAG = "FragmentedMp4Extractor"; @SuppressWarnings("ConstantCaseForConstants") - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); + 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}; 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/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index bbf7e2fc6b..ff32ae3462 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,7 +33,7 @@ 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; 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/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..0a0755327c 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 @@ -27,7 +27,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,7 +44,7 @@ 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 static final int ID3_TAG = 0x00494433; private final long firstSampleTimestampUs; private final Ac3Reader reader; 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..4db02e0d83 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 @@ -29,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; /** Extracts data from AC-4 bitstreams. */ @@ -53,7 +52,7 @@ 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 static final int ID3_TAG = 0x00494433; private final long firstSampleTimestampUs; private final Ac4Reader reader; 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..a636d2f680 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 @@ -32,7 +32,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 +65,7 @@ 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"); + private static final int ID3_TAG = 0x00494433; /** * 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. 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/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index c7b7a40ead..7a6a7e346f 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. */ @@ -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")) { + final int data = 0x64617461; + while (chunkHeader.id != data) { 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")) { + final int riff = 0x52494646; + if (chunkHeader.id == riff) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { 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..4417126427 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. */ 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/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 9211dc51ce..89017a40c0 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; 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 5e425cc12f..b977f61a8a 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 @@ -31,13 +31,13 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int BOX_HEADER_SIZE = 8; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); + private static final int TYPE_payl = 0x7061796c; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg"); + private static final int TYPE_sttg = 0x73747467; @SuppressWarnings("ConstantCaseForConstants") - private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); + private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; private final WebvttCue.Builder builder; 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. From 6abd5dc66f93b088693e351e4514fea296291924 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 22 May 2019 09:17:49 +0100 Subject: [PATCH 050/807] Add missing annotations dependency Issue: #5926 PiperOrigin-RevId: 249404152 --- extensions/ima/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a91bbbd981..2df9448d08 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,6 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.0.2' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } From a4d18a7457c9e5cdd7528189666c7ca8ca4d45f9 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 22 May 2019 11:27:57 +0100 Subject: [PATCH 051/807] Remove mistakenly left link in vp9 readme PiperOrigin-RevId: 249417898 --- extensions/vp9/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 0de29eea32..2c5b64f8bd 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -66,7 +66,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 ## From d836957138ba56db4d6459dbe87210ce24d97166 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 11:43:37 +0100 Subject: [PATCH 052/807] Remove some DataSource implementations from nullness blacklist PiperOrigin-RevId: 249419193 --- .../exoplayer2/upstream/AssetDataSource.java | 14 +++++---- .../upstream/ByteArrayDataSink.java | 15 ++++++---- .../upstream/ByteArrayDataSource.java | 9 +++--- .../upstream/ContentDataSource.java | 24 ++++++++++----- .../upstream/DataSchemeDataSource.java | 18 ++++++++---- .../exoplayer2/upstream/DummyDataSource.java | 7 +++-- .../exoplayer2/upstream/FileDataSource.java | 21 ++++++++++---- .../upstream/RawResourceDataSource.java | 29 +++++++++++++------ .../exoplayer2/upstream/UdpDataSource.java | 13 +++++---- .../upstream/crypto/AesCipherDataSink.java | 19 +++++++----- .../upstream/crypto/AesCipherDataSource.java | 6 ++-- .../upstream/crypto/CryptoUtil.java | 8 +++-- 12 files changed, 119 insertions(+), 64 deletions(-) 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 eeb0f1c957..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; @@ -55,7 +58,7 @@ public final class AssetDataSource extends BaseDataSource { 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("/")) { @@ -101,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); } @@ -121,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/ByteArrayDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java index 4017c1f028..a9f9da0a95 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; + @MonotonicNonNull private 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 8df69ffb7a..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; @@ -60,13 +62,18 @@ public final class ContentDataSource extends BaseDataSource { @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) { @@ -110,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); } @@ -130,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..03804fa577 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; + @Nullable private DataSpec dataSpec; + @Nullable private byte[] data; + private int dataLength; private int bytesRead; - private @Nullable byte[] data; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -54,15 +57,17 @@ public final class DataSchemeDataSource extends BaseDataSource { if (uriParts[0].contains(";base64")) { try { data = Base64.decode(dataString, 0); + dataLength = data.length; } catch (IllegalArgumentException e) { throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); } } else { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); + dataLength = data.length; } transferStarted(dataSpec); - return data.length; + return dataLength; } @Override @@ -70,19 +75,20 @@ public final class DataSchemeDataSource extends BaseDataSource { if (readLength == 0) { return 0; } - int remainingBytes = data.length - bytesRead; + int remainingBytes = dataLength - bytesRead; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(data, bytesRead, buffer, offset, readLength); + System.arraycopy(castNonNull(data), bytesRead, buffer, offset, readLength); bytesRead += readLength; bytesTransferred(readLength); return readLength; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } 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 cead366360..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; @@ -48,9 +51,13 @@ public final class FileDataSource extends BaseDataSource { @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; @@ -76,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); } @@ -91,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/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 7b70bcc5c4..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; @@ -81,21 +84,28 @@ public final class RawResourceDataSource extends BaseDataSource { @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) { @@ -133,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); } @@ -153,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/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index fcfeef3fb4..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; @@ -144,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/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..644338c8eb 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,7 +66,7 @@ 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; } 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; } From 10ee7d8e861132a0d937ebf585a437ff491cf1b4 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 13:40:12 +0100 Subject: [PATCH 053/807] Remove more classes from nullness blacklist PiperOrigin-RevId: 249431027 --- .../exoplayer2/scheduler/Requirements.java | 3 +- .../scheduler/RequirementsWatcher.java | 10 ++++--- .../exoplayer2/text/CaptionStyleCompat.java | 30 ++++++++++++------- .../google/android/exoplayer2/text/Cue.java | 25 +++++++--------- .../exoplayer2/text/SubtitleOutputBuffer.java | 3 +- .../exoplayer2/text/webvtt/CssParser.java | 27 ++++++++++------- .../google/android/exoplayer2/util/Util.java | 15 ++++++++++ 7 files changed, 71 insertions(+), 42 deletions(-) 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..882d9def3c 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,7 @@ 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.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -179,7 +180,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..b9cbf681b1 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(); @@ -139,7 +141,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/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..29facdb210 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; @@ -111,17 +112,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 +295,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -376,7 +373,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -403,9 +400,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/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 75b7a01673..b34628b922 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,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; import java.util.List; @@ -24,7 +25,7 @@ import java.util.List; */ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle { - private Subtitle subtitle; + @Nullable private Subtitle subtitle; private long subsampleOffsetUs; /** 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..193b92678b 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,11 @@ */ 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.regex.Matcher; import java.util.regex.Pattern; @@ -52,13 +52,15 @@ 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 {@link WebvttCssStyle} instance if successful, or {@code null} + * otherwise. * * @param input The input from which the style block should be read. - * @return A {@link WebvttCssStyle} that represents the parsed block. + * @return A {@link WebvttCssStyle} that represents the parsed block, or {@code null} if parsing + * failed. */ + @Nullable public WebvttCssStyle parseBlock(ParsableByteArray input) { stringBuilder.setLength(0); int initialInputPosition = input.getPosition(); @@ -86,13 +88,14 @@ import java.util.regex.Pattern; } /** - * 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) { @@ -116,7 +119,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; @@ -196,6 +199,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 +241,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; @@ -325,7 +330,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/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 97bcb68708..4dfb8b50d5 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 @@ -315,6 +315,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. * From f74d2294be0160fe1391b420a4e357c2dce5baf7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 13:47:59 +0100 Subject: [PATCH 054/807] Remove media-session extension nullness blacklist PiperOrigin-RevId: 249431620 --- extensions/mediasession/build.gradle | 1 + .../mediasession/MediaSessionConnector.java | 84 ++++++++++++------- .../RepeatModeActionProvider.java | 3 +- library/core/build.gradle | 1 - 4 files changed, 57 insertions(+), 32 deletions(-) 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 d03e8fbdbf..9ec3886df5 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}. @@ -359,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 @@ -676,6 +677,7 @@ public final class MediaSessionConnector { */ public final void invalidateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + Player player = this.player; if (player == null) { builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); mediaSession.setPlaybackState(builder.build()); @@ -749,8 +751,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); } } @@ -760,18 +762,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() { @@ -829,29 +835,43 @@ 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; } @@ -941,38 +961,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); + 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) { + 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) { + 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()); + Bitmap iconBitmap = description.getIconBitmap(); + if (iconBitmap != null) { + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap); } - if (description.getIconUri() != null) { + 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()); + String mediaId = description.getMediaId(); + if (mediaId != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId); } - if (description.getMediaUri() != null) { + 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; } @@ -993,6 +1015,7 @@ public final class MediaSessionConnector { @Override public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + Player player = Assertions.checkNotNull(MediaSessionConnector.this.player); int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { @@ -1035,6 +1058,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); 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/library/core/build.gradle b/library/core/build.gradle index f532ae0e6a..5b285411d0 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -60,7 +60,6 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion From 118218cc73efecb62d1ae3be39a490eaca5edd5c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 May 2019 13:55:46 +0100 Subject: [PATCH 055/807] Remove cronet extension nullness blacklist PiperOrigin-RevId: 249432337 --- extensions/cronet/build.gradle | 1 + .../ext/cronet/CronetDataSource.java | 148 ++++++++++-------- library/core/build.gradle | 1 + 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 76972a3530..0808ad6c44 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -34,6 +34,7 @@ dependencies { api 'org.chromium.net:cronet-embedded:73.3683.76' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') } 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..0ef20e79bd 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; @@ -118,7 +121,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { 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; @@ -130,18 +133,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; @@ -197,7 +200,8 @@ 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 The optional default {@link RequestProperties} to be sent to + * the server as HTTP headers on every request. */ public CronetDataSource( CronetEngine cronetEngine, @@ -206,7 +210,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties) { + @Nullable RequestProperties defaultRequestProperties) { this( cronetEngine, executor, @@ -232,7 +236,8 @@ 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 The 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. */ @@ -243,7 +248,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties, + @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { this( cronetEngine, @@ -265,7 +270,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, - RequestProperties defaultRequestProperties, + @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { super(/* isNetwork= */ true); this.urlRequestCallback = new UrlRequestCallback(); @@ -305,6 +310,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } @Override + @Nullable public Uri getUri() { return responseInfo == null ? null : Uri.parse(responseInfo.getUrl()); } @@ -317,22 +323,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 +347,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 +355,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)); } @@ -358,8 +366,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { 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); } } @@ -378,7 +386,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,15 +405,17 @@ 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); + castNonNull(currentUrlRequest).read(readBuffer); try { if (!operation.block(readTimeoutMs)) { throw new SocketTimeoutException(); @@ -413,20 +423,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } 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; + this.readBuffer = null; Thread.currentThread().interrupt(); throw new HttpDataSourceException( - new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ); + new InterruptedIOException(e), + castNonNull(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); + this.readBuffer = null; + throw new HttpDataSourceException( + e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } if (exception != null) { - throw new HttpDataSourceException(exception, currentDataSpec, - HttpDataSourceException.TYPE_READ); + throw new HttpDataSourceException( + exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } else if (finished) { bytesRemaining = 0; return C.RESULT_END_OF_INPUT; @@ -631,7 +644,8 @@ 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(); } @@ -643,13 +657,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 +674,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/library/core/build.gradle b/library/core/build.gradle index 5b285411d0..f532ae0e6a 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -60,6 +60,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion From cfefdbc134101e6efca35bd5b781f7eaa8020c7d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 22 May 2019 14:54:41 +0100 Subject: [PATCH 056/807] Release DownloadHelper automatically if preparation failed. This prevents further unexpected updates if the MediaSource happens to finish its preparation at a later point. Issue:#5915 PiperOrigin-RevId: 249439246 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/offline/DownloadHelper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed9635a340..06c1ed7f80 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,9 @@ ([#5891](https://github.com/google/ExoPlayer/issues/5891)). * Add ProgressUpdateListener to PlayerControlView ([#5834](https://github.com/google/ExoPlayer/issues/5834)). +* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the + preparation of the `DownloadHelper` failed + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). ### 2.10.1 ### 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..e7cf87ed6e 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 @@ -943,6 +943,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: From 073256ead06d96b60ad878a07f93af930e5a259f Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 22 May 2019 19:44:46 +0100 Subject: [PATCH 057/807] improve issue templates PiperOrigin-RevId: 249489446 --- .github/ISSUE_TEMPLATE/bug.md | 9 ++++++--- .github/ISSUE_TEMPLATE/content_not_playing.md | 5 ++++- .github/ISSUE_TEMPLATE/feature_request.md | 5 +++-- .github/ISSUE_TEMPLATE/question.md | 8 ++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 690069ffa8..a4996278bd 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: diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index f326e7cd46..ff29f3a7d1 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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 089de35910..d481de33ce 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,8 +8,9 @@ assignees: '' Before filing a feature request: ----------------------- -- Search existing open issues, specifically with the label ‘enhancement’. -- Search existing pull requests. +- Search existing open issues, specifically with the label ‘enhancement’: + https://github.com/google/ExoPlayer/labels/enhancement +- Search existing pull requests: https://github.com/google/ExoPlayer/pulls When filing a feature request: ----------------------- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 3ed569862f..a68e4e70e1 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -12,8 +12,12 @@ Before filing a question: a general Android development question, please do so on Stack Overflow. - Search existing issues, including issues that are closed. It’s often the quickest way to get an answer! -- Consult our FAQs, developer guide and the class reference of ExoPlayer. These - can be found at https://exoplayer.dev/. + 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, devices as well as + information about how to use the ExoPlayer library. +- The ExoPlayer library Javadoc can be found at + https://exoplayer.dev/doc/reference/ When filing a question: ----------------------- From 2f12374f1a4128a7844c3c6d804e08f0ecb7b53f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 May 2019 10:56:58 +0100 Subject: [PATCH 058/807] Fix IndexOutOfBounds when there are no available codecs PiperOrigin-RevId: 249610014 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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..730868987a 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; /** @@ -742,11 +741,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) { From 8d329fb41f19cb9303f90158c6b84ced1af955d3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 May 2019 13:24:01 +0100 Subject: [PATCH 059/807] Move DefaultDrmSession resource acquisition to acquire PiperOrigin-RevId: 249624318 --- .../exoplayer2/drm/DefaultDrmSession.java | 39 ++++++++++--------- .../drm/OfflineLicenseHelperTest.java | 7 ++++ 2 files changed, 27 insertions(+), 19 deletions(-) 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..94f5affb39 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 @@ -74,7 +74,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,7 +85,7 @@ 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; @@ -104,10 +104,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private @DrmSession.State int state; private int openCount; - private HandlerThread requestHandlerThread; - private PostRequestHandler postRequestHandler; - private @Nullable T mediaCrypto; - private @Nullable DrmSessionException lastException; + @Nullable private HandlerThread requestHandlerThread; + @Nullable private PostRequestHandler postRequestHandler; + @Nullable private T mediaCrypto; + @Nullable private DrmSessionException lastException; private byte @MonotonicNonNull [] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; @@ -166,35 +166,31 @@ 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; - } + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); 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) { + // Assigning null to various non-null variables for clean-up. state = STATE_RELEASED; postResponseHandler.removeCallbacksAndMessages(null); - postRequestHandler.removeCallbacksAndMessages(null); + Util.castNonNull(postRequestHandler).removeCallbacksAndMessages(null); postRequestHandler = null; - requestHandlerThread.quit(); + Util.castNonNull(requestHandlerThread).quit(); requestHandlerThread = null; mediaCrypto = null; lastException = null; @@ -227,7 +223,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() { @@ -335,7 +335,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. " @@ -398,7 +398,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/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 83ca752114..d6b0b5ba15 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,6 +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.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; @@ -25,6 +27,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.util.HashMap; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -46,6 +49,10 @@ public class OfflineLicenseHelperTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); + when(mediaDrm.getKeyRequest( + nullable(byte[].class), nullable(List.class), anyInt(), nullable(HashMap.class))) + .thenReturn( + new ExoMediaDrm.KeyRequest(/* data= */ new byte[0], /* licenseServerUrl= */ "")); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); } From 3314391932394feef441d6f619651c6ea5f3d5f7 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 May 2019 13:29:56 +0100 Subject: [PATCH 060/807] Add basic DRM support to CastPlayer's demo app PiperOrigin-RevId: 249624829 --- RELEASENOTES.md | 1 + .../DefaultReceiverPlayerManager.java | 46 +++++++++++++-- .../android/exoplayer2/castdemo/DemoUtil.java | 56 ++++++++++--------- .../exoplayer2/castdemo/MainActivity.java | 13 ++++- .../ext/cast/DefaultCastOptionsProvider.java | 26 ++++++++- 5 files changed, 108 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 06c1ed7f80..deda085f40 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Add basic DRM support to the Cast demo app. * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s ([#5779](https://github.com/google/ExoPlayer/issues/5779)). * Assume that encrypted content requires secure decoders in renderer support diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index fcee88ec49..a837bd77e5 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -44,11 +44,14 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; /** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ /* package */ class DefaultReceiverPlayerManager @@ -394,12 +397,47 @@ import java.util.ArrayList; private static MediaQueueItem buildMediaQueueItem(MediaItem item) { MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo mediaInfo = + MediaInfo.Builder mediaInfoBuilder = new MediaInfo.Builder(item.media.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) - .setMetadata(movieMetadata) - .build(); - return new MediaQueueItem.Builder(mediaInfo).build(); + .setMetadata(movieMetadata); + if (!item.drmSchemes.isEmpty()) { + MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + try { + // This configuration is only intended for testing and should *not* be used in production + // environments. See comment in the Cast Demo app's options provider. + JSONObject drmConfiguration = getDrmConfigurationJson(scheme); + if (drmConfiguration != null) { + mediaInfoBuilder.setCustomData(drmConfiguration); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); + } + + @Nullable + private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + throws JSONException { + String drmScheme; + if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + drmScheme = "playready"; + } else { + return null; + } + MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); + JSONObject exoplayerConfig = + new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); + if (!licenseServer.uri.equals(Uri.EMPTY)) { + exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + } + if (!licenseServer.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + } + return new JSONObject().put("exoPlayerConfig", exoplayerConfig); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 9625304252..9599da15cb 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.castdemo; -import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ @@ -30,44 +30,25 @@ import java.util.UUID; /** Represents a media sample. */ public static final class Sample { - /** The uri of the media content. */ + /** The URI of the media content. */ public final String uri; /** The name of the sample. */ public final String name; /** The mime type of the sample media content. */ public final String mimeType; - /** - * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not - * DRM-protected. - */ - @Nullable public final UUID drmSchemeUuid; - /** - * The url from which players should obtain DRM licenses, or null if the content is not - * DRM-protected. - */ - @Nullable public final Uri licenseServerUri; + /** Data to configure DRM license acquisition. May be null if content is not DRM-protected. */ + @Nullable public final DrmConfiguration drmConfiguration; - /** - * @param uri See {@link #uri}. - * @param name See {@link #name}. - * @param mimeType See {@link #mimeType}. - */ public Sample(String uri, String name, String mimeType) { - this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null); + this(uri, name, mimeType, /* drmConfiguration= */ null); } public Sample( - String uri, - String name, - String mimeType, - @Nullable UUID drmSchemeUuid, - @Nullable String licenseServerUriString) { + String uri, String name, String mimeType, @Nullable DrmConfiguration drmConfiguration) { this.uri = uri; this.name = name; this.mimeType = mimeType; - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = - licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null; + this.drmConfiguration = drmConfiguration; } @Override @@ -76,6 +57,29 @@ import java.util.UUID; } } + /** Holds information required to play DRM-protected content. */ + public static final class DrmConfiguration { + + /** The {@link UUID} of the DRM scheme that protects the content. */ + public final UUID drmSchemeUuid; + /** + * The URI from which players should obtain DRM licenses. May be null if the license server URI + * is provided as part of the media. + */ + @Nullable public final String licenseServerUri; + /** HTTP request headers to include the in DRM license requests. */ + public final Map httpRequestHeaders; + + public DrmConfiguration( + UUID drmSchemeUuid, + @Nullable String licenseServerUri, + Map httpRequestHeaders) { + this.drmSchemeUuid = drmSchemeUuid; + this.licenseServerUri = licenseServerUri; + this.httpRequestHeaders = httpRequestHeaders; + } + } + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 17eeed2da7..5ed434eed6 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; +import android.net.Uri; import android.os.Bundle; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; @@ -36,6 +37,7 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; @@ -121,6 +123,7 @@ public class MainActivity extends AppCompatActivity String applicationId = castContext.getCastOptions().getReceiverApplicationId(); switch (applicationId) { case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: + case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM: playerManager = new DefaultReceiverPlayerManager( /* listener= */ this, @@ -202,11 +205,17 @@ public class MainActivity extends AppCompatActivity .setMedia(sample.uri) .setTitle(sample.name) .setMimeType(sample.mimeType); - if (sample.drmSchemeUuid != null) { + DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; + if (drmConfiguration != null) { mediaItemBuilder.setDrmSchemes( Collections.singletonList( new MediaItem.DrmScheme( - sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri)))); + drmConfiguration.drmSchemeUuid, + new MediaItem.UriBundle( + drmConfiguration.licenseServerUri != null + ? Uri.parse(drmConfiguration.licenseServerUri) + : Uri.EMPTY, + drmConfiguration.httpRequestHeaders)))); } playerManager.addItem(mediaItemBuilder.build()); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); 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..5aed1373e5 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 @@ -27,11 +27,33 @@ 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 From 14c46bc4062ebc2cf45f96138dc8a5e36bf41da5 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 May 2019 14:59:17 +0100 Subject: [PATCH 061/807] Remove contentTypePredicate from DataSource constructors The only known use case for contentTypePredicate is to catch the case when a paywall web page is returned via a DataSource, rather than the data that was being requested. These days streaming providers should be using HTTPS, where this problem does not exist. Devices have also gotten a lot better at showing their own notifications when paywalls are detected, which largely mitigates the need for the app to show a more optimal error message or redirect the user to a browser. It therefore makes sense to deprioritize this feature. In particular by removing the arg from constructors, where nearly all applications are probably passing null. PiperOrigin-RevId: 249634594 --- .../ext/cronet/CronetDataSource.java | 117 +++++++++++++++--- .../ext/cronet/CronetDataSourceFactory.java | 83 +++++-------- .../ext/cronet/CronetDataSourceTest.java | 38 +++--- .../ext/okhttp/OkHttpDataSource.java | 58 +++++++-- .../ext/okhttp/OkHttpDataSourceFactory.java | 1 - .../upstream/DefaultHttpDataSource.java | 75 ++++++++++- .../DefaultHttpDataSourceFactory.java | 1 - 7 files changed, 270 insertions(+), 103 deletions(-) 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 0ef20e79bd..dd10e5bb66 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 @@ -116,7 +116,6 @@ 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; @@ -126,6 +125,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private final ConditionVariable operation; private final Clock clock; + @Nullable private Predicate contentTypePredicate; + // Accessed by the calling thread only. private boolean opened; private long bytesToSkip; @@ -158,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); } /** @@ -171,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, @@ -182,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); } /** @@ -200,9 +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 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 #CronetDataSource(CronetEngine, Executor, int, int, boolean, + * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -218,9 +295,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, - Clock.DEFAULT, defaultRequestProperties, - false); + /* handleSetCookieRequests= */ false); } /** @@ -236,11 +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 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. * @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, @@ -253,19 +332,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { 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, @@ -276,7 +354,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { 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; @@ -287,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 @@ -363,6 +451,7 @@ 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); 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/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..df36076899 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 @@ -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); } } @@ -734,7 +737,6 @@ public final class CronetDataSourceTest { new CronetDataSource( mockCronetEngine, mockExecutor, - mockContentTypePredicate, TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, true, // resetTimeoutOnRedirects @@ -765,13 +767,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 +805,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(); 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..eaa305875b 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,6 +155,17 @@ 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() { 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..f18e37c5c4 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 @@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory { new OkHttpDataSource( callFactory, userAgent, - /* contentTypePredicate= */ null, cacheControl, defaultRequestProperties); if (listener != null) { 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 65b65efe2c..5955a5d9d9 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 @@ -74,13 +74,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 +91,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 +142,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 +163,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 +195,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, @@ -164,6 +216,17 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou this.defaultRequestProperties = defaultRequestProperties; } + /** + * 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() { return connection == null ? null : Uri.parse(connection.getURL().toString()); 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..e0b1efad54 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 @@ -107,7 +107,6 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { DefaultHttpDataSource dataSource = new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, From 3e990a3d24e28eeaefd57f7e5bfb5b3522f05adb Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 May 2019 16:54:45 +0100 Subject: [PATCH 062/807] Fix nullness warning for MediaSource/MediaPeriod classes. PiperOrigin-RevId: 249652301 --- .../source/AbstractConcatenatedTimeline.java | 7 ++- .../source/ClippingMediaPeriod.java | 29 ++++++----- .../source/ClippingMediaSource.java | 9 ++-- .../source/DeferredMediaPeriod.java | 41 ++++++++------- .../source/ExtractorMediaSource.java | 16 +++--- .../exoplayer2/source/MergingMediaPeriod.java | 35 ++++++++----- .../exoplayer2/source/MergingMediaSource.java | 8 +-- .../source/SingleSampleMediaPeriod.java | 21 +++++--- .../source/ads/AdPlaybackState.java | 33 +++++++----- .../exoplayer2/source/ads/AdsMediaSource.java | 51 ++++++++++--------- 10 files changed, 146 insertions(+), 104 deletions(-) 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/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..c3e700fff5 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,9 @@ 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 Object manifest; + @Nullable private ClippingTimeline clippingTimeline; + @Nullable private IllegalClippingException clippingError; private long periodStartUs; private long periodEndUs; @@ -222,7 +223,7 @@ 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); } } 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/DeferredMediaPeriod.java index abf02541c8..95a218bfe7 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/DeferredMediaPeriod.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 @@ -47,10 +50,10 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb 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; @@ -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/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 3951dc20a2..841f18bab4 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 @@ -242,8 +242,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 +264,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 +296,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, 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..7188cada0f 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,9 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Object primaryManifest; + @Nullable private Object primaryManifest; private int periodCount; - private IllegalMergeException mergeError; + @Nullable private IllegalMergeException mergeError; /** * @param mediaSources The {@link MediaSource}s to merge. @@ -170,11 +170,13 @@ public final class MergingMediaSource extends CompositeMediaSource { } @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/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index e0c2a00df3..6063168e99 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. @@ -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/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..78b0f6de11 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 @@ -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()); } } @@ -131,12 +132,12 @@ public final class AdsMediaSource extends CompositeMediaSource { 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 Object contentManifest; + @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 @@ -202,24 +203,25 @@ 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]; + if (mediaSource == null) { + mediaSource = adMediaSourceFactory.createMediaSource(adUri); + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; + deferredMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); + prepareChildSource(id, mediaSource); + } DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); deferredMediaPeriod.setPrepareErrorListener( @@ -227,7 +229,8 @@ public final class AdsMediaSource extends CompositeMediaSource { List mediaPeriods = deferredMediaPeriodByAdMediaSource.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); } else { @@ -258,7 +261,7 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releaseSourceInternal() { super.releaseSourceInternal(); - componentListener.release(); + Assertions.checkNotNull(componentListener).release(); componentListener = null; deferredMediaPeriodByAdMediaSource.clear(); contentTimeline = null; @@ -305,7 +308,7 @@ public final class AdsMediaSource extends CompositeMediaSource { maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + private void onContentSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest) { Assertions.checkArgument(timeline.getPeriodCount() == 1); contentTimeline = timeline; contentManifest = manifest; @@ -330,6 +333,7 @@ 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 = @@ -340,7 +344,8 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - 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]; From 11c0c6d2662c5a7c04ae61f436d1dcbdd655a5b9 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 24 May 2019 13:53:22 +0100 Subject: [PATCH 063/807] Reset upstream format when empty track selection happens PiperOrigin-RevId: 249819080 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 1 + 1 file changed, 1 insertion(+) 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..434b6c2011 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 @@ -322,6 +322,7 @@ import java.util.Map; if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; + pendingResetUpstreamFormats = true; mediaChunks.clear(); if (loader.isLoading()) { if (sampleQueuesBuilt) { From 3afdd7ac5ab48284a481e6bdabe562e3a2d814b0 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 24 May 2019 15:19:05 +0100 Subject: [PATCH 064/807] Put @Nullable annotation in the right place PiperOrigin-RevId: 249828748 --- .../exoplayer2/demo/PlayerActivity.java | 3 ++- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 2 +- .../ext/flac/FlacExtractorSeekTest.java | 3 ++- .../exoplayer2/ext/flac/FlacExtractor.java | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 8 +++---- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 3 ++- .../ext/leanback/LeanbackPlayerAdapter.java | 6 ++--- .../ext/okhttp/OkHttpDataSource.java | 3 ++- .../ext/okhttp/OkHttpDataSourceFactory.java | 6 ++--- .../ext/rtmp/RtmpDataSourceFactory.java | 2 +- .../android/exoplayer2/DefaultMediaClock.java | 4 ++-- .../android/exoplayer2/ExoPlayerImpl.java | 5 +++-- .../exoplayer2/ExoPlayerImplInternal.java | 2 +- .../com/google/android/exoplayer2/Format.java | 20 ++++++++--------- .../android/exoplayer2/MediaPeriodQueue.java | 8 +++---- .../android/exoplayer2/PlaybackInfo.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 5 +++-- .../android/exoplayer2/SimpleExoPlayer.java | 3 ++- .../analytics/AnalyticsCollector.java | 22 ++++++++++++------- .../analytics/AnalyticsListener.java | 2 +- .../exoplayer2/audio/AudioFocusManager.java | 2 +- .../audio/AudioTimestampPoller.java | 2 +- .../audio/AudioTrackPositionTracker.java | 6 ++--- .../exoplayer2/drm/DefaultDrmSession.java | 14 +++++++----- .../drm/DefaultDrmSessionManager.java | 6 ++--- .../android/exoplayer2/drm/DrmInitData.java | 9 ++++---- .../exoplayer2/drm/ErrorStateDrmSession.java | 12 ++++++---- .../extractor/amr/AmrExtractor.java | 2 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 2 +- .../exoplayer2/extractor/mp4/Atom.java | 6 +++-- .../extractor/mp4/FragmentedMp4Extractor.java | 8 +++---- .../extractor/ts/AdtsExtractor.java | 2 +- .../mediacodec/MediaCodecRenderer.java | 2 +- .../mediacodec/MediaCodecSelector.java | 3 ++- .../exoplayer2/metadata/MetadataRenderer.java | 2 +- .../exoplayer2/metadata/id3/ApicFrame.java | 2 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 8 ++++--- .../metadata/id3/TextInformationFrame.java | 2 +- .../exoplayer2/metadata/id3/UrlLinkFrame.java | 2 +- .../source/CompositeMediaSource.java | 4 ++-- .../source/ExtractorMediaSource.java | 6 ++--- .../source/MediaSourceEventListener.java | 6 ++--- .../source/ProgressiveMediaPeriod.java | 2 +- .../source/SinglePeriodTimeline.java | 2 +- .../source/SingleSampleMediaPeriod.java | 2 +- .../source/SingleSampleMediaSource.java | 4 ++-- .../exoplayer2/source/chunk/Chunk.java | 2 +- .../source/chunk/ChunkSampleStream.java | 2 +- .../android/exoplayer2/text/TextRenderer.java | 2 +- .../AdaptiveTrackSelection.java | 5 +++-- .../trackselection/FixedTrackSelection.java | 7 +++--- .../trackselection/MappingTrackSelector.java | 2 +- .../trackselection/RandomTrackSelection.java | 3 ++- .../trackselection/TrackSelectionArray.java | 3 ++- .../trackselection/TrackSelector.java | 4 ++-- .../exoplayer2/upstream/BaseDataSource.java | 2 +- .../android/exoplayer2/upstream/DataSpec.java | 6 ++--- .../upstream/DefaultDataSource.java | 17 +++++++------- .../upstream/DefaultDataSourceFactory.java | 2 +- .../upstream/DefaultHttpDataSource.java | 3 ++- .../DefaultHttpDataSourceFactory.java | 2 +- .../upstream/FileDataSourceFactory.java | 2 +- .../android/exoplayer2/upstream/Loader.java | 2 +- .../upstream/PriorityDataSource.java | 3 ++- .../exoplayer2/upstream/StatsDataSource.java | 3 ++- .../exoplayer2/upstream/TeeDataSource.java | 3 ++- .../upstream/cache/CacheDataSource.java | 15 +++++++------ .../exoplayer2/upstream/cache/CacheSpan.java | 6 ++--- .../upstream/crypto/AesCipherDataSource.java | 3 ++- .../exoplayer2/util/EGLSurfaceTexture.java | 10 ++++----- .../android/exoplayer2/util/EventLogger.java | 2 +- .../exoplayer2/util/ParsableByteArray.java | 6 +++-- .../exoplayer2/util/TimedValueQueue.java | 3 ++- .../android/exoplayer2/video/ColorInfo.java | 2 +- .../exoplayer2/video/DummySurface.java | 6 ++--- .../android/exoplayer2/video/HevcConfig.java | 2 +- .../video/MediaCodecVideoRenderer.java | 2 +- .../video/spherical/CameraMotionRenderer.java | 2 +- .../analytics/AnalyticsCollectorTest.java | 2 +- .../upstream/BaseDataSourceTest.java | 3 ++- .../source/dash/DashMediaPeriod.java | 4 ++-- .../source/dash/DashMediaSource.java | 6 ++--- .../source/dash/DefaultDashChunkSource.java | 2 +- .../source/dash/manifest/RangedUri.java | 3 ++- .../source/hls/Aes128DataSource.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 4 ++-- .../exoplayer2/source/hls/HlsMediaSource.java | 4 ++-- .../playlist/DefaultHlsPlaylistTracker.java | 3 ++- .../source/smoothstreaming/SsMediaPeriod.java | 4 ++-- .../source/smoothstreaming/SsMediaSource.java | 4 ++-- .../android/exoplayer2/ui/DefaultTimeBar.java | 6 ++--- .../android/exoplayer2/ui/PlayerView.java | 3 ++- .../ui/spherical/ProjectionRenderer.java | 4 ++-- .../ui/spherical/SceneRenderer.java | 2 +- .../ui/spherical/SphericalSurfaceView.java | 8 +++---- .../exoplayer2/ui/spherical/TouchTracker.java | 2 +- .../android/exoplayer2/testutil/Action.java | 4 ++-- .../exoplayer2/testutil/ActionSchedule.java | 2 +- .../testutil/ExoPlayerTestRunner.java | 6 ++--- .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../exoplayer2/testutil/FakeDataSet.java | 6 ++--- .../exoplayer2/testutil/FakeMediaSource.java | 2 +- .../exoplayer2/testutil/FakeSampleStream.java | 2 +- 103 files changed, 247 insertions(+), 206 deletions(-) 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..f7db8c7ca3 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 @@ -555,7 +555,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 { 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..12c26ca2ec 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; 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/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..79350e6ae3 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 @@ -88,7 +88,7 @@ public final class FlacExtractor implements Extractor { private FlacStreamInfo streamInfo; private Metadata id3Metadata; - private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker; + @Nullable private FlacBinarySearchSeeker flacBinarySearchSeeker; private boolean readPastStreamInfo; 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..1cdbac56b5 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; 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/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..1fece6bc8e 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; 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 eaa305875b..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 @@ -167,7 +167,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } @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 f18e37c5c4..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 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/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..bcec6426d6 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 @@ -43,8 +43,8 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; private final StandaloneMediaClock standaloneMediaClock; private final PlaybackParameterListener listener; - private @Nullable Renderer rendererClockSource; - private @Nullable MediaClock rendererClock; + @Nullable private Renderer rendererClockSource; + @Nullable private MediaClock rendererClock; /** * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use 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..de6be33686 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 @@ -73,7 +73,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private boolean foregroundMode; 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 +199,8 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { return playbackError; } 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..ff94567a48 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 @@ -1836,7 +1836,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; 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..cf1c6f4e5a 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 @@ -45,9 +45,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 +57,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 +72,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 +84,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 +122,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,7 +157,7 @@ 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. */ 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..58000d6f58 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 @@ -46,11 +46,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. */ 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..d3e4a0e626 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 @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** The current {@link Timeline}. */ public final Timeline timeline; /** The current manifest. */ - public final @Nullable Object manifest; + @Nullable public final Object manifest; /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; /** 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..da66f3dd10 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 @@ -880,7 +880,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { verifyApplicationThread(); return player.getPlaybackError(); } 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..deecfb15a8 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 @@ -687,8 +687,8 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; - private @Nullable MediaPeriodInfo lastReportedPlayingMediaPeriod; - private @Nullable MediaPeriodInfo readingMediaPeriod; + @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; + @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; private boolean isSeeking; @@ -706,7 +706,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 +720,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 +729,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 +739,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 +761,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); 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/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java index 3cc05e87df..2d65b64f36 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,7 +103,7 @@ 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; 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/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 94f5affb39..e300c65592 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 @@ -88,13 +88,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; 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; @@ -111,8 +111,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private byte @MonotonicNonNull [] 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. @@ -259,12 +259,14 @@ 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; } 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..7481c60c64 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; 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/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 82fd9a5549..bcc0739042 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,22 +34,26 @@ 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; } 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/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 9bfe383169..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 @@ -458,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); @@ -478,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/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index e0673dd4fa..392d4d9179 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 @@ -122,11 +122,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; @@ -139,13 +139,13 @@ public class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray scratch; // Adjusts sample timestamps. - private final @Nullable TimestampAdjuster timestampAdjuster; + @Nullable private final TimestampAdjuster timestampAdjuster; // 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; 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 a636d2f680..d1e3217e30 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 @@ -85,7 +85,7 @@ public final class AdtsExtractor implements Extractor { private final ParsableBitArray scratchBits; private final long firstStreamSampleTimestampUs; - private @Nullable ExtractorOutput extractorOutput; + @Nullable private ExtractorOutput extractorOutput; private long firstSampleTimestampUs; private long firstFramePosition; 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 730868987a..cd043655ec 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 @@ -94,7 +94,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * 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) { 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/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index e34b4074fb..a72c70442e 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 @@ -48,7 +48,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; 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 4417126427..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 @@ -82,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); @@ -97,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()); } @@ -110,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/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 9323f7505c..06db088f06 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,8 +34,8 @@ 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. */ protected CompositeMediaSource() { 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 841f18bab4..d9003e443e 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 @@ -64,9 +64,9 @@ public final class ExtractorMediaSource extends BaseMediaSource 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; 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/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index d9f0008a7f..e8f630f202 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 @@ -1013,7 +1013,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/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index acdfbcc8c0..14648775f8 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,7 @@ 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; /** * Creates a timeline containing a single period and a window that spans it. 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 6063168e99..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 @@ -53,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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; 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..55d967cd69 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. 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/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 18eada4708..d7a19fa9d4 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 @@ -76,7 +76,7 @@ public class ChunkSampleStream implements SampleStream, S private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; - private @Nullable ReleaseCallback releaseCallback; + @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; 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..bdf127be59 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 @@ -77,7 +77,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/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index bbf57c5602..08f4d3a928 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; @@ -537,7 +537,8 @@ public class AdaptiveTrackSelection 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/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/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/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/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index a98f773c9d..99a3d271bd 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 @@ -97,10 +97,10 @@ public final class DataSpec { /** * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. */ - public final @Nullable byte[] httpBody; + @Nullable public final byte[] httpBody; /** @deprecated Use {@link #httpBody} instead. */ - @Deprecated public final @Nullable byte[] postBody; + @Deprecated @Nullable public final byte[] postBody; /** * The absolute position of the data in the full stream. @@ -121,7 +121,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; 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 aa13baa03e..bfc9a37844 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 @@ -62,14 +62,14 @@ 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 dataSchemeDataSource; + @Nullable private DataSource rawResourceDataSource; - private @Nullable DataSource dataSource; + @Nullable private DataSource dataSource; /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. @@ -178,7 +178,8 @@ public final class DefaultDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } 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 a5dfad72f0..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; /** 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 5955a5d9d9..0d16a3f20e 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 @@ -228,7 +228,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } 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 e0b1efad54..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; 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/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/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/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 58b2d176cf..058489f8f0 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,16 @@ 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; + @Nullable private Uri uri; + @Nullable private Uri actualUri; private @HttpMethod int httpMethod; 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; @@ -329,7 +329,8 @@ public final class CacheDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return actualUri; } 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/crypto/AesCipherDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 644338c8eb..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 @@ -71,7 +71,8 @@ public final class AesCipherDataSource 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/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/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/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/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..7193c4c22b 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 @@ -138,7 +138,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. 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/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 2e9b539096..22aa63b83a 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 @@ -847,7 +847,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/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/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..be0aa4f154 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 @@ -69,7 +69,7 @@ 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 LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -82,7 +82,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; 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..779a97fd09 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 @@ -373,11 +373,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; @@ -1139,7 +1139,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, 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 057f0262d0..2de81a2535 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 @@ -617,7 +617,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; 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..9ac1257ee2 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 @@ -86,7 +86,8 @@ public final class RangedUri { * @param baseUri The optional 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/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..022d62cbfc 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}. 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..d834c097cf 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 @@ -62,7 +62,7 @@ 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 LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -72,7 +72,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; 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..fbb6285d1d 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 @@ -301,9 +301,9 @@ public final class HlsMediaSource extends BaseMediaSource 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, 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..a4fd28009f 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,7 +166,8 @@ public final class DefaultHlsPlaylistTracker } @Override - public @Nullable HlsMasterPlaylist getMasterPlaylist() { + @Nullable + public HlsMasterPlaylist getMasterPlaylist() { return masterPlaylist; } 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..38781782eb 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 @@ -42,7 +42,7 @@ import java.util.List; 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 LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; @@ -50,7 +50,7 @@ import java.util.List; 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; 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..3c18bfe644 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 @@ -323,12 +323,12 @@ public final class SsMediaSource extends BaseMediaSource 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; 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 5c70203788..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,8 +217,8 @@ 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); 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 5bb8324780..3575969a78 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 @@ -621,7 +621,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; } 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..8a211d0879 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 @@ -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 8f82ae17db..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 @@ -64,14 +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 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); 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/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..93e52bc23a 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. @@ -547,7 +547,7 @@ public abstract class Action { */ public static final class WaitForTimelineChanged extends Action { - private final @Nullable Timeline expectedTimeline; + @Nullable private final Timeline expectedTimeline; /** * Creates action waiting for a timeline change. 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/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 0d55dd8530..cc4b3a60d7 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 @@ -338,9 +338,9 @@ 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; 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..fea863c48e 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 @@ -40,7 +40,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; 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/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index b89acae6c8..0d50f22bc0 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 @@ -60,7 +60,7 @@ public class FakeMediaSource extends BaseMediaSource { 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 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; From 2c4eb1cd9bff6dd39a51a946872de79635ba19ea Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 28 May 2019 11:30:23 +0100 Subject: [PATCH 065/807] Demo app change. PiperOrigin-RevId: 250248268 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 f7db8c7ca3..82fb8bb9f5 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 @@ -151,7 +151,8 @@ public class PlayerActivity extends AppCompatActivity @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); } From 6b68bc0c9d977e3cace2b5c13184563d971ba249 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 12:20:14 +0100 Subject: [PATCH 066/807] Fix anchor usage in SubtitlePainter's setupBitmapLayout According to Cue's constructor (for bitmaps) documentation: + cuePositionAnchor does horizontal anchoring. + cueLineAnchor does vertical anchoring. Usage is currently inverted. Issue:#5633 PiperOrigin-RevId: 250253002 --- .../android/exoplayer2/ui/SubtitlePainter.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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); } From 47cc567dcaf26bf9ee673e0346317dc15a80ac1e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 15:33:57 +0100 Subject: [PATCH 067/807] Add reference counting to DrmSession This CL should not introduce any functional changes. PiperOrigin-RevId: 250277165 --- .../ext/vp9/LibvpxVideoRenderer.java | 20 ++--- .../android/exoplayer2/FormatHolder.java | 6 +- .../audio/SimpleDecoderAudioRenderer.java | 20 ++--- .../exoplayer2/drm/DefaultDrmSession.java | 82 ++++++++++--------- .../drm/DefaultDrmSessionManager.java | 11 +-- .../android/exoplayer2/drm/DrmSession.java | 28 +++++++ .../exoplayer2/drm/DrmSessionManager.java | 12 +-- .../exoplayer2/drm/ErrorStateDrmSession.java | 9 ++ .../exoplayer2/drm/OfflineLicenseHelper.java | 4 +- .../mediacodec/MediaCodecRenderer.java | 20 ++--- 10 files changed, 105 insertions(+), 107 deletions(-) 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..5871371d76 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 @@ -473,21 +473,13 @@ 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); - } } /** @@ -512,12 +504,10 @@ public class LibvpxVideoRenderer extends BaseRenderer { } 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); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); } - setSourceDrmSession(session); + sourceDrmSession = session; } else { setSourceDrmSession(null); } 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..bd73eaf4fb 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,7 +16,7 @@ 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}. @@ -25,14 +25,14 @@ public final class FormatHolder { /** * Whether the object expected to populate {@link #format} is also expected to populate {@link - * #decryptionResource}. + * #drmSession}. */ // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal // ref: b/129764794]. public boolean decryptionResourceIsProvided; /** 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/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 553dfb1187..1553227988 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 @@ -646,21 +646,13 @@ 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 { @@ -677,12 +669,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } 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); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); } - setSourceDrmSession(session); + sourceDrmSession = session; } else { setSourceDrmSession(null); } 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 e300c65592..e49602957f 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,6 +38,7 @@ 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; @@ -103,12 +104,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /* package */ final PostResponseHandler postResponseHandler; private @DrmSession.State int state; - private int openCount; + private int referenceCount; @Nullable private HandlerThread requestHandlerThread; @Nullable private PostRequestHandler postRequestHandler; @Nullable private T mediaCrypto; @Nullable private DrmSessionException lastException; - private byte @MonotonicNonNull [] sessionId; + private byte @NullableType [] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; @Nullable private KeyRequest currentKeyRequest; @@ -169,42 +170,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; postResponseHandler = new PostResponseHandler(playbackLooper); } - // Life cycle. - - public void acquire() { - if (++openCount == 1) { - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - if (openInternal(true)) { - doLicense(true); - } - } - } - - @SuppressWarnings("assignment.type.incompatible") - public void release() { - if (--openCount == 0) { - // Assigning null to various non-null variables for clean-up. - state = STATE_RELEASED; - 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); - } - } - public boolean hasSessionId(byte[] sessionId) { return Arrays.equals(this.sessionId, sessionId); } @@ -270,6 +235,42 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; 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. /** @@ -288,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) { @@ -329,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: @@ -364,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()) { 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 7481c60c64..84e984445a 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 @@ -432,19 +432,10 @@ 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(); - } - // ProvisioningManager implementation. @Override 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..761fb74287 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 @@ -28,6 +28,20 @@ import java.util.Map; */ public interface DrmSession { + /** + * Invokes {@code newSession's} {@link #acquireReference()} and {@code previousSession's} {@link + * #releaseReference()} in that order. Does nothing for passed null values. + */ + static void replaceSessionReferences( + @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (newSession != null) { + newSession.acquireReference(); + } + if (previousSession != null) { + previousSession.releaseReference(); + } + } + /** * Wraps the throwable which is the cause of the error state. */ @@ -110,4 +124,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..168783cf1c 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 @@ -34,8 +34,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 @@ -43,10 +45,4 @@ public interface DrmSessionManager { * @return The DRM session. */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); - - /** - * Releases a {@link DrmSession}. - */ - void releaseSession(DrmSession drmSession); - } 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 bcc0739042..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 @@ -57,4 +57,13 @@ public final class ErrorStateDrmSession implements Drm 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/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/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index cd043655ec..05f83109e8 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 @@ -941,21 +941,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); - } } /** @@ -1159,12 +1151,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } 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); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); } - setSourceDrmSession(session); + sourceDrmSession = session; } else { setSourceDrmSession(null); } From c495a3f55e7a6ee1bfe653068967d7300d07acc0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 May 2019 16:36:09 +0100 Subject: [PATCH 068/807] Fix VP9 build setup Update configuration script to use an external build, so we can remove use of isysroot which is broken in the latest NDK r19c. Also switch from gnustl_static to c++_static so that ndk-build with NDK r19c succeeds. Issue: #5922 PiperOrigin-RevId: 250287551 --- extensions/vp9/README.md | 3 +- extensions/vp9/src/main/jni/Application.mk | 4 +- .../jni/generate_libvpx_android_configs.sh | 44 ++++++------------- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 2c5b64f8bd..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: 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 From 04f38885501f8f6091c35d7a091e6dd43ded95c8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 May 2019 17:26:19 +0100 Subject: [PATCH 069/807] Add allowOnlyClearBuffers to SampleQueue#read Removes the need for duplicate calls to SampleQueue#read when implementing DecryptionResources acquisition in the MediaSources. PiperOrigin-RevId: 250298175 --- .../source/ProgressiveMediaPeriod.java | 7 +- .../source/SampleMetadataQueue.java | 7 ++ .../exoplayer2/source/SampleQueue.java | 14 ++- .../source/chunk/ChunkSampleStream.java | 14 ++- .../exoplayer2/source/SampleQueueTest.java | 114 ++++++++++++++++-- .../source/dash/PlayerEmsgHandler.java | 9 +- .../source/hls/HlsSampleStreamWrapper.java | 7 +- 7 files changed, 154 insertions(+), 18 deletions(-) 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 e8f630f202..dbf5f8aa5d 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 @@ -447,7 +447,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; maybeNotifyDownstreamFormat(track); int result = sampleQueues[track].read( - formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { maybeStartDeferredRetry(track); } 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..b2c09bd70f 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 @@ -230,6 +230,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 +244,7 @@ import com.google.android.exoplayer2.util.Util; FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean allowOnlyClearBuffers, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { @@ -264,6 +267,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()) { 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..976a5d4e48 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 @@ -324,6 +324,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 +336,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/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index d7a19fa9d4..d9b28d9c92 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 @@ -409,7 +409,12 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyPrimaryTrackFormatChanged(); return primarySampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + decodeOnlyUntilPositionUs); } @Override @@ -801,7 +806,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/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 450f0ecd3a..bfc6bb52c9 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; @@ -511,6 +515,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 +649,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 +765,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 +786,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 +815,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 +849,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 +872,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/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/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 434b6c2011..96704053cb 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 @@ -491,7 +491,12 @@ import java.util.Map; int result = sampleQueues[sampleQueueIndex].read( - formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); + formatHolder, + buffer, + requireFormat, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (sampleQueueIndex == primarySampleQueueIndex) { From 90325c699e735bcd80b92ed6d7386ce75585f8b8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 May 2019 17:40:50 +0100 Subject: [PATCH 070/807] Allow enabling decoder fallback in DefaultRenderersFactory Also allow enabling decoder fallback with MediaCodecAudioRenderer. Issue: #5942 PiperOrigin-RevId: 250301422 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultRenderersFactory.java | 30 +++++++++++++- .../audio/MediaCodecAudioRenderer.java | 40 ++++++++++++++++++- .../testutil/DebugRenderersFactory.java | 1 + 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index deda085f40..edddbbe7ec 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,8 @@ * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed ([#5915](https://github.com/google/ExoPlayer/issues/5915)). +* Allow enabling decoder fallback with `DefaultRenderersFactory` + ([#5942](https://github.com/google/ExoPlayer/issues/5942)). ### 2.10.1 ### 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/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index c3ec759c2d..d83e32c61c 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; 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..6bd4c8dd14 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 @@ -56,6 +56,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, From 8a0fb6b78f64005fe0d1403e0ad42b601a221bd9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 May 2019 10:09:54 +0100 Subject: [PATCH 071/807] Fix video size reporting in surface YUV mode In surface YUV output mode the width/height fields of the VpxOutputBuffer were never populated. Fix this by adding a new method to set the width/height and calling it from JNI like we do for GL YUV mode. PiperOrigin-RevId: 250449734 --- .../android/exoplayer2/ext/vp9/VpxOutputBuffer.java | 13 +++++++++++-- extensions/vp9/src/main/jni/vpx_jni.cc | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) 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..30d7b8e92c 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 @@ -60,8 +60,8 @@ public final class VpxOutputBuffer extends OutputBuffer { * 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}. + * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, {@link + * VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}. */ public void init(long timeUs, int mode) { this.timeUs = timeUs; @@ -110,6 +110,15 @@ public final class VpxOutputBuffer extends OutputBuffer { 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; + } + private void initData(int size) { if (data == null || data.capacity() < size) { data = ByteBuffer.allocateDirect(size); 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); } From 8912db5ed9df8624ce8574f99b3b2972193a7656 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 29 May 2019 10:12:27 +0100 Subject: [PATCH 072/807] Remove DecryptionResource Reference count was built into DrmSession PiperOrigin-RevId: 250449988 --- .../exoplayer2/drm/DecryptionResource.java | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java 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."); - } - } -} From 94d668567c27e66fc9441f5fe1eccd42e83bbd2c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 May 2019 11:26:08 +0100 Subject: [PATCH 073/807] Migrate org.mockito.Matchers#any* to org.mockito.ArgumentMatchers The former is deprecated and replaced by the latter in Mockito 2. However, there is a functional difference: ArgumentMatchers will reject `null` and check the type if the matcher specified a type (e.g. `any(Class)` or `anyInt()`). `any()` will remain to accept anything. PiperOrigin-RevId: 250458607 --- .../android/exoplayer2/ext/cronet/CronetDataSourceTest.java | 4 ++-- .../android/exoplayer2/drm/OfflineLicenseHelperTest.java | 2 +- .../exoplayer2/trackselection/AdaptiveTrackSelectionTest.java | 2 +- .../exoplayer2/upstream/cache/CachedRegionTrackerTest.java | 4 ++-- .../android/exoplayer2/source/hls/HlsMediaPeriodTest.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) 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 df36076899..a01c5e84b6 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; 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 d6b0b5ba15..886be4c476 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,9 +17,9 @@ package com.google.android.exoplayer2.drm; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import android.util.Pair; 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..91e7393fe7 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; 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/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..f389944670 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,7 +15,7 @@ */ 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; From 09b00c7fb06253d6171a6098ad4bdb82a5a7c9d7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 May 2019 18:25:27 +0100 Subject: [PATCH 074/807] Allow passthrough of E-AC3-JOC streams PiperOrigin-RevId: 250517338 --- .../java/com/google/android/exoplayer2/C.java | 7 +++-- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- .../audio/MediaCodecAudioRenderer.java | 31 +++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 3 +- 4 files changed, 37 insertions(+), 7 deletions(-) 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..8ded5038b0 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 @@ -146,8 +146,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 +163,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 +211,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 */ 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..425b0994b6 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 @@ -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); 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 d83e32c61c..fe8e898b06 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 @@ -374,7 +374,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 @@ -471,11 +471,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); @@ -497,6 +500,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 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: From 1151848f590bd3a68f2e2d96395903789c0dbd53 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 May 2019 18:33:25 +0100 Subject: [PATCH 075/807] No-op move of span touching into helper method PiperOrigin-RevId: 250519114 --- .../upstream/cache/SimpleCache.java | 116 ++++++++++-------- 1 file changed, 63 insertions(+), 53 deletions(-) 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..38c43bd551 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 @@ -408,30 +408,9 @@ public final class SimpleCache implements Cache { 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); @@ -558,36 +537,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 +645,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. * From 71418f9411daa0f131091c18f480132fef8ad061 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 29 May 2019 18:36:01 +0100 Subject: [PATCH 076/807] Fix TTML bitmap subtitles + Use start for anchoring, instead of center. + Add the height to the TTML bitmap cue rendering layout. Issue:#5633 PiperOrigin-RevId: 250519710 --- RELEASENOTES.md | 7 +++++-- .../android/exoplayer2/text/ttml/TtmlDecoder.java | 1 + .../google/android/exoplayer2/text/ttml/TtmlNode.java | 4 ++-- .../android/exoplayer2/text/ttml/TtmlRegion.java | 4 ++++ .../android/exoplayer2/text/ttml/TtmlDecoderTest.java | 10 +++++----- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index edddbbe7ec..3b1ccc3d43 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,8 +9,11 @@ checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* CEA-608: Handle XDS and TEXT modes - ([5807](https://github.com/google/ExoPlayer/pull/5807)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * Audio: * Fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). 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..6e0c495466 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 @@ -429,6 +429,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/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..85af6482c0 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 From ed5ce2396d50672522f0f9888969ad8f2243208b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 May 2019 19:23:42 +0100 Subject: [PATCH 077/807] SimpleCache: Tweak comments related to blocking "Write case, lock not available" was a bit confusing. When the content is not cached and the lock is held, it's neither a read or a write case. It's a "can't do anything" case. When blocking, it may subsequently turn into either a read or a write. PiperOrigin-RevId: 250530722 --- .../exoplayer2/upstream/cache/SimpleCache.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 38c43bd551..1d4481b5cc 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 @@ -390,10 +390,11 @@ public final class SimpleCache implements Cache { 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(); } } @@ -415,12 +416,12 @@ public final class SimpleCache implements Cache { 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; } From 42ba6abf5a548c05120c586edf2af411f579ca14 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 29 May 2019 20:42:25 +0100 Subject: [PATCH 078/807] Fix CacheUtil.cache() use too much data cache() opens all connections with unset length to avoid position errors. This makes more data then needed to be downloading by the underlying network stack. This fix makes makes it open connections for only required length. Issue:#5927 PiperOrigin-RevId: 250546175 --- RELEASENOTES.md | 11 ++-- .../upstream/cache/CacheDataSource.java | 18 +----- .../exoplayer2/upstream/cache/CacheUtil.java | 63 +++++++++++++------ .../exoplayer2/testutil/CacheAsserts.java | 16 +++-- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b1ccc3d43..b73d95ba03 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,16 +26,19 @@ ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). -* Offline: Add Scheduler implementation which uses WorkManager. +* Offline: + * Add Scheduler implementation which uses WorkManager. + * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the + preparation of the `DownloadHelper` failed + ([#5915](https://github.com/google/ExoPlayer/issues/5915)). + * Fix CacheUtil.cache() use too much data + ([#5927](https://github.com/google/ExoPlayer/issues/5927)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). * Add ProgressUpdateListener to PlayerControlView ([#5834](https://github.com/google/ExoPlayer/issues/5834)). -* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the - preparation of the `DownloadHelper` failed - ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). 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 058489f8f0..69bb99451e 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 @@ -136,7 +136,7 @@ public final class CacheDataSource implements DataSource { private boolean currentDataSpecLengthUnset; @Nullable private Uri uri; @Nullable private Uri actualUri; - private @HttpMethod int httpMethod; + @HttpMethod private int httpMethod; private int flags; @Nullable private String key; private long readPosition; @@ -319,7 +319,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; } @@ -485,20 +485,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/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 219d736835..9c80becdeb 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; @@ -195,37 +196,42 @@ public final class CacheUtil { 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; + } } } @@ -242,6 +248,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,6 +262,7 @@ public final class CacheUtil { PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -263,22 +271,23 @@ public final class CacheUtil { // 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; + try { + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, length)); + } catch (IOException exception) { + if (length == C.LENGTH_UNSET + || !isLastBlock + || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); + // Retry to open the data source again, setting length to C.LENGTH_UNSET to prevent + // getting an error in case the given length exceeds the end of input. + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); + } + if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } long totalBytesRead = 0; @@ -340,6 +349,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/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 9a17904379..a48f88b5c0 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -83,7 +83,8 @@ public final class CacheAsserts { * @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); + // TODO Make tests specify if the content length is stored in cache metadata. + DataSpec dataSpec = new DataSpec(uri, 0, expected.length, null, 0); assertDataCached(cache, dataSpec, expected); } @@ -95,15 +96,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); } /** From e2452f8103a2a45eeeaeab385b2c3005ebad13a8 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 10:40:00 +0100 Subject: [PATCH 079/807] Simplify CacheUtil PiperOrigin-RevId: 250654697 --- .../exoplayer2/upstream/cache/CacheUtil.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) 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 9c80becdeb..5b066b7930 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 @@ -79,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) { @@ -180,22 +174,19 @@ 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); @@ -235,6 +226,17 @@ public final class CacheUtil { } } + 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; + } + } + /** * Reads and discards all data specified by the {@code dataSpec}. * From 00b26a51df6bfcd0422cd4d1747c8f9490e0611f Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 10:47:28 +0100 Subject: [PATCH 080/807] Modify DashDownloaderTest to test if content length is stored PiperOrigin-RevId: 250655481 --- .../dash/offline/DashDownloaderTest.java | 11 +- .../dash/offline/DownloadManagerDashTest.java | 7 +- .../source/hls/offline/HlsDownloaderTest.java | 25 +++-- .../exoplayer2/testutil/CacheAsserts.java | 102 +++++++++++------- 4 files changed, 90 insertions(+), 55 deletions(-) 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/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/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/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index a48f88b5c0..4ea4c0844e 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -33,59 +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 { - // TODO Make tests specify if the content length is stored in cache metadata. - DataSpec dataSpec = new DataSpec(uri, 0, expected.length, null, 0); - assertDataCached(cache, dataSpec, expected); + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + assertCachedData(cache, new RequestSet(fakeDataSet)); } /** From b8ec05aea19be5eb3ef317a7daf10ed1d36154b4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 11:42:20 +0100 Subject: [PATCH 081/807] Handle gzip in DefaultHttpDataSource. Setting the requested encoding in all cases ensures we receive the relevant response headers indicating whether gzip was used. Doing that allows to detect the content length in cases where gzip was requested, but the server replied with uncompressed content. PiperOrigin-RevId: 250660890 --- .../ext/cronet/CronetDataSource.java | 6 +++--- .../upstream/DefaultHttpDataSource.java | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) 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 dd10e5bb66..a1ee80767d 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 @@ -466,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 { @@ -626,7 +626,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)) { @@ -655,7 +655,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; } - private static boolean getIsCompressed(UrlResponseInfo info) { + private static boolean isCompressed(UrlResponseInfo info) { for (Map.Entry entry : info.getAllHeadersAsList()) { if (entry.getKey().equalsIgnoreCase("Content-Encoding")) { return !entry.getValue().equalsIgnoreCase("identity"); 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 0d16a3f20e..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,6 +41,7 @@ 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}. @@ -305,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 { @@ -315,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); @@ -516,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, @@ -747,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); + } } From 6e7012413bf0f6ae7f3885caa1001988cc5d5f89 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 11:54:15 +0100 Subject: [PATCH 082/807] Keep controller visible on d-pad key events PiperOrigin-RevId: 250661977 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ui/PlayerView.java | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b73d95ba03..f98e3dfe84 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,8 @@ `PlayerControlView`. * Change playback controls toggle from touch down to touch up events ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: 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 3575969a78..7e01801daf 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 @@ -772,11 +772,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; From c09a6eb8eed94bebd224d7eb68062eced1e688e2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 12:19:33 +0100 Subject: [PATCH 083/807] Update cast extension build PiperOrigin-RevId: 250664791 --- extensions/cast/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 4dc463ff81..e067789bc4 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.1.2' + api 'com.google.android.gms:play-services-cast-framework:16.2.0' implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') From 77595da1592836bd988d3a6f9bba473701e30456 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:01:33 +0100 Subject: [PATCH 084/807] Add initial PlaybackStats listener version. This version includes all playback state related metrics and the general listener set-up. PiperOrigin-RevId: 250668729 --- .../exoplayer2/analytics/PlaybackStats.java | 567 ++++++++++++++++ .../analytics/PlaybackStatsListener.java | 614 ++++++++++++++++++ 2 files changed, 1181 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java 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..c30d2ac854 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -0,0 +1,567 @@ +/* + * 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.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; + +/** 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 pauseCount = 0; + int pauseBufferCount = 0; + int seekCount = 0; + int rebufferCount = 0; + long maxRebufferTimeMs = C.TIME_UNSET; + int adCount = 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; + pauseCount += stats.totalPauseCount; + pauseBufferCount += stats.totalPauseBufferCount; + seekCount += stats.totalSeekCount; + rebufferCount += stats.totalRebufferCount; + if (maxRebufferTimeMs == C.TIME_UNSET) { + maxRebufferTimeMs = stats.maxRebufferTimeMs; + } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) { + maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs); + } + adCount += stats.adPlaybackCount; + } + return new PlaybackStats( + playbackCount, + playbackStateDurationsMs, + /* playbackStateHistory */ Collections.emptyList(), + firstReportedTimeMs, + foregroundPlaybackCount, + abandonedBeforeReadyCount, + endedCount, + backgroundJoiningCount, + totalValidJoinTimeMs, + validJoinTimeCount, + pauseCount, + pauseBufferCount, + seekCount, + rebufferCount, + maxRebufferTimeMs, + adCount); + } + + /** 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 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; + + private final long[] playbackStateDurationsMs; + + /* package */ PlaybackStats( + int playbackCount, + long[] playbackStateDurationsMs, + List> playbackStateHistory, + 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) { + this.playbackCount = playbackCount; + this.playbackStateDurationsMs = playbackStateDurationsMs; + this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); + 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; + } + + /** + * 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. + */ + @PlaybackState + public 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 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(); + } +} 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..12fc40e817 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -0,0 +1,614 @@ +/* + * 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.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.util.Assertions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@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; + + /** + * 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; + 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); + 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 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); + } + } + } + + /** 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 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; + + // Current player state tracking. + @PlaybackState private 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; + + /** + * 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(); + 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(); + } + + /** + * 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) { + 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); + } + + /** + * 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; + 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); + } + 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; + return new PlaybackStats( + /* playbackCount= */ 1, + playbackStateDurationsMs, + isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + 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); + } + + 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++; + } + + maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); + + currentPlaybackState = newPlaybackState; + currentPlaybackStateStartTimeMs = eventTime.realtimeMs; + if (keepHistory) { + playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + } + } + + @PlaybackState + private 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 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; + } + } +} From fd1179aaa14b4973ba335fe001ac0224678f6b02 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:02:01 +0100 Subject: [PATCH 085/807] Add remaining PlaybackStatsListener metrics. This adds all the non-playback-state metrics, like format, error, bandwidth and renderer performance metrics. PiperOrigin-RevId: 250668854 --- RELEASENOTES.md | 2 + .../com/google/android/exoplayer2/Format.java | 32 ++ .../exoplayer2/analytics/PlaybackStats.java | 435 +++++++++++++++++- .../analytics/PlaybackStatsListener.java | 390 +++++++++++++++- 4 files changed, 842 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f98e3dfe84..83a323ec62 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis + and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s ([#5779](https://github.com/google/ExoPlayer/issues/5779)). 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 cf1c6f4e5a..a482022a17 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 @@ -1373,6 +1373,38 @@ public final class Format implements Parcelable { accessibilityChannel); } + 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); + } + /** * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height} * are known, or {@link #NO_VALUE} otherwise diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java index c30d2ac854..f633bfbf8e 100644 --- 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 @@ -19,6 +19,7 @@ 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; @@ -27,6 +28,7 @@ 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 { @@ -109,12 +111,31 @@ public final class PlaybackStats { int backgroundJoiningCount = 0; long totalValidJoinTimeMs = C.TIME_UNSET; int validJoinTimeCount = 0; - int pauseCount = 0; - int pauseBufferCount = 0; - int seekCount = 0; - int rebufferCount = 0; + int totalPauseCount = 0; + int totalPauseBufferCount = 0; + int totalSeekCount = 0; + int totalRebufferCount = 0; long maxRebufferTimeMs = C.TIME_UNSET; - int adCount = 0; + 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++) { @@ -135,21 +156,53 @@ public final class PlaybackStats { totalValidJoinTimeMs += stats.totalValidJoinTimeMs; } validJoinTimeCount += stats.validJoinTimeCount; - pauseCount += stats.totalPauseCount; - pauseBufferCount += stats.totalPauseBufferCount; - seekCount += stats.totalSeekCount; - rebufferCount += stats.totalRebufferCount; + 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); } - adCount += stats.adPlaybackCount; + 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, @@ -157,12 +210,35 @@ public final class PlaybackStats { backgroundJoiningCount, totalValidJoinTimeMs, validJoinTimeCount, - pauseCount, - pauseBufferCount, - seekCount, - rebufferCount, + totalPauseCount, + totalPauseBufferCount, + totalSeekCount, + totalRebufferCount, maxRebufferTimeMs, - adCount); + 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. */ @@ -175,6 +251,12 @@ public final class PlaybackStats { * 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. @@ -223,12 +305,108 @@ public final class PlaybackStats { /** 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, @@ -241,10 +419,34 @@ public final class PlaybackStats { int totalSeekCount, int totalRebufferCount, long maxRebufferTimeMs, - int adPlaybackCount) { + 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; @@ -258,6 +460,29 @@ public final class PlaybackStats { 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); } /** @@ -289,6 +514,41 @@ public final class PlaybackStats { 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 @@ -564,4 +824,147 @@ public final class PlaybackStats { 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 index 12fc40e817..e7410668e2 100644 --- 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 @@ -20,6 +20,8 @@ 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; @@ -27,13 +29,20 @@ 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. @@ -72,6 +81,7 @@ public final class PlaybackStatsListener @Nullable private String activeAdPlayback; private boolean playWhenReady; @Player.State private int playbackState; + private float playbackSpeed; /** * Creates listener for playback stats. @@ -89,6 +99,7 @@ public final class PlaybackStatsListener finishedPlaybackStats = PlaybackStats.EMPTY; playWhenReady = false; playbackState = Player.STATE_IDLE; + playbackSpeed = 1f; period = new Period(); sessionManager.setListener(this); } @@ -158,6 +169,7 @@ public final class PlaybackStatsListener 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); } @@ -286,6 +298,27 @@ public final class PlaybackStatsListener } } + @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) { @@ -297,6 +330,88 @@ public final class PlaybackStatsListener } } + @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 { @@ -304,6 +419,11 @@ public final class PlaybackStatsListener 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; @@ -315,6 +435,21 @@ public final class PlaybackStatsListener 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. @PlaybackState private int currentPlaybackState; @@ -327,6 +462,11 @@ public final class PlaybackStatsListener 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. @@ -338,12 +478,21 @@ public final class PlaybackStatsListener 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; } /** @@ -407,6 +556,10 @@ public final class PlaybackStatsListener * @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; @@ -446,6 +599,115 @@ public final class PlaybackStatsListener 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. * @@ -453,6 +715,7 @@ public final class PlaybackStatsListener */ public PlaybackStats build(boolean isFinal) { long[] playbackStateDurationsMs = this.playbackStateDurationsMs; + List mediaTimeHistory = this.mediaTimeHistory; if (!isFinal) { long buildTimeMs = SystemClock.elapsedRealtime(); playbackStateDurationsMs = @@ -460,6 +723,12 @@ public final class PlaybackStatsListener 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 = @@ -472,6 +741,7 @@ public final class PlaybackStatsListener /* playbackCount= */ 1, playbackStateDurationsMs, isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + mediaTimeHistory, firstReportedTimeMs, /* foregroundPlaybackCount= */ isForeground ? 1 : 0, /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, @@ -484,7 +754,30 @@ public final class PlaybackStatsListener seekCount, rebufferCount, maxRebufferTimeMs, - /* adPlaybackCount= */ isAd ? 1 : 0); + /* adPlaybackCount= */ isAd ? 1 : 0, + isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory), + isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory), + 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) { @@ -517,7 +810,12 @@ public final class PlaybackStatsListener 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; @@ -581,6 +879,96 @@ public final class PlaybackStatsListener } } + 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; From 126aad58822b7212bf3d74a322e2b2e61aa7e2d9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 13:40:49 +0100 Subject: [PATCH 086/807] Rename host_activity.xml to avoid manifest merge conflicts. PiperOrigin-RevId: 250672752 --- .../com/google/android/exoplayer2/testutil/HostActivity.java | 3 ++- .../{host_activity.xml => exo_testutils_host_activity.xml} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename testutils/src/main/res/layout/{host_activity.xml => exo_testutils_host_activity.xml} (100%) 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/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 From f9d6f314e2b15e8e348fa57a64d02f5950b6159f Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 30 May 2019 14:41:03 +0100 Subject: [PATCH 087/807] Fix misreporting cached bytes when caching is paused When caching is resumed, it starts from the initial position. This makes more data to be reported as cached. Issue:#5573 PiperOrigin-RevId: 250678841 --- RELEASENOTES.md | 2 + .../exoplayer2/upstream/cache/CacheUtil.java | 44 +++++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 83a323ec62..b6cbe3d275 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ ([#5915](https://github.com/google/ExoPlayer/issues/5915)). * Fix CacheUtil.cache() use too much data ([#5927](https://github.com/google/ExoPlayer/issues/5927)). + * Fix misreporting cached bytes when caching is paused + ([#5573](https://github.com/google/ExoPlayer/issues/5573)). * Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector 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 5b066b7930..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 @@ -268,6 +268,8 @@ public final class CacheUtil { 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. @@ -275,45 +277,51 @@ public final class CacheUtil { } throwExceptionIfInterruptedOrCancelled(isCanceled); try { - long resolvedLength; - try { - resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, length)); - } catch (IOException exception) { - if (length == C.LENGTH_UNSET - || !isLastBlock - || !isCausedByPositionOutOfRange(exception)) { - throw exception; + 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); } - Util.closeQuietly(dataSource); - // Retry to open the data source again, setting length to C.LENGTH_UNSET to prevent - // getting an error in case the given length exceeds the end of input. + } + 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 { From 090f359895825db92ecc48c2f13429dd90e53db8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 May 2019 15:08:51 +0100 Subject: [PATCH 088/807] Make parallel adaptive track selection more robust. Using parallel adaptation for Formats without bitrate information currently causes an exception. Handle this gracefully and also cases where all formats have the same bitrate. Issue:#5971 PiperOrigin-RevId: 250682127 --- RELEASENOTES.md | 3 +++ .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b6cbe3d275..cb63af693d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,9 @@ ([#5834](https://github.com/google/ExoPlayer/issues/5834)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). +* Fix bug caused by parallel adaptive track selection using `Format`s without + bitrate information + ([#5971](https://github.com/google/ExoPlayer/issues/5971)). ### 2.10.1 ### 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 08f4d3a928..ca8a0b12f9 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 @@ -758,7 +758,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; @@ -780,7 +780,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; From 7e187283cd4a08f4915124ba586f895a4773fb20 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 May 2019 18:53:19 +0100 Subject: [PATCH 089/807] Add MediaSource-provided-DRM support to Renderer implementations PiperOrigin-RevId: 250719155 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 15 ++++++++++----- .../audio/SimpleDecoderAudioRenderer.java | 15 ++++++++++----- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 15 ++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) 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 5871371d76..f5d92e2a15 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 @@ -489,6 +489,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. */ @CallSuper + @SuppressWarnings("unchecked") protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; @@ -502,12 +503,16 @@ public class LibvpxVideoRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (sourceDrmSession != null) { - sourceDrmSession.releaseReference(); + if (formatHolder.decryptionResourceIsProvided) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - sourceDrmSession = session; } else { setSourceDrmSession(null); } 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 1553227988..08e8203fd4 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 @@ -655,6 +655,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements decoderDrmSession = session; } + @SuppressWarnings("unchecked") private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = inputFormat; inputFormat = newFormat; @@ -667,12 +668,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (sourceDrmSession != null) { - sourceDrmSession.releaseReference(); + if (formatHolder.decryptionResourceIsProvided) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - sourceDrmSession = session; } else { setSourceDrmSession(null); } 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 05f83109e8..6d0f9a4aad 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 @@ -1135,6 +1135,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; @@ -1149,12 +1150,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (sourceDrmSession != null) { - sourceDrmSession.releaseReference(); + if (formatHolder.decryptionResourceIsProvided) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - sourceDrmSession = session; } else { setSourceDrmSession(null); } From b47f37fbcd8a15dfef38bd7e5afccd2231a65d76 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 May 2019 23:11:19 +0100 Subject: [PATCH 090/807] Add HlsTrackMetadataEntry.toString It's printed out by EventLogger, and currently looks pretty ugly PiperOrigin-RevId: 250772010 --- .../android/exoplayer2/source/hls/HlsTrackMetadataEntry.java | 5 +++++ 1 file changed, 5 insertions(+) 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) { From a9de1477ee9018063b5cb1f08e11c780a6d82396 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 14:11:16 +0100 Subject: [PATCH 091/807] Bump to 2.10.2 PiperOrigin-RevId: 251216822 --- RELEASENOTES.md | 29 ++++++++++--------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++-- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cb63af693d..3be8c4ed50 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,27 +11,21 @@ checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* Subtitles: - * CEA-608: Handle XDS and TEXT modes +* CEA-608: Handle XDS and TEXT modes ([#5807](https://github.com/google/ExoPlayer/pull/5807)). - * TTML: Fix bitmap rendering - ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * Audio: * Fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). * Add `SilenceMediaSource` that can be used to play silence of a given duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). -* UI: - * Allow setting `DefaultTimeBar` attributes on `PlayerView` and - `PlayerControlView`. - * Change playback controls toggle from touch down to touch up events - ([#5784](https://github.com/google/ExoPlayer/issues/5784)). - * Fix issue where playback controls were not kept visible on key presses - ([#5963](https://github.com/google/ExoPlayer/issues/5963)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Offline: * Add Scheduler implementation which uses WorkManager. + +### 2.10.2 ### + +* Offline: * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed ([#5915](https://github.com/google/ExoPlayer/issues/5915)). @@ -39,11 +33,20 @@ ([#5927](https://github.com/google/ExoPlayer/issues/5927)). * Fix misreporting cached bytes when caching is paused ([#5573](https://github.com/google/ExoPlayer/issues/5573)). -* Add a playWhenReady flag to MediaSessionConnector.PlaybackPreparer methods +* UI: + * Allow setting `DefaultTimeBar` attributes on `PlayerView` and + `PlayerControlView`. + * Change playback controls toggle from touch down to touch up events + ([#5784](https://github.com/google/ExoPlayer/issues/5784)). + * Fix issue where playback controls were not kept visible on key presses + ([#5963](https://github.com/google/ExoPlayer/issues/5963)). +* TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector ([#5891](https://github.com/google/ExoPlayer/issues/5891)). -* Add ProgressUpdateListener to PlayerControlView +* Add `ProgressUpdateListener` to `PlayerControlView` ([#5834](https://github.com/google/ExoPlayer/issues/5834)). * Allow enabling decoder fallback with `DefaultRenderersFactory` ([#5942](https://github.com/google/ExoPlayer/issues/5942)). diff --git a/constants.gradle b/constants.gradle index 6e4cd58d09..3fe22a2762 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.1' - releaseVersionCode = 2010001 + releaseVersion = '2.10.2' + releaseVersionCode = 2010002 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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..db3f3943e1 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.2"; /** 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.2"; /** * 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 = 2010002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 871a88a921564d727f8b2429631d9ed6825f8524 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 19:09:58 +0100 Subject: [PATCH 092/807] Clean up release notes PiperOrigin-RevId: 251269746 --- RELEASENOTES.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3be8c4ed50..f284376cfb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,26 +5,22 @@ * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. -* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s - ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Offline: Add `Scheduler` implementation that uses `WorkManager`. * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* CEA-608: Handle XDS and TEXT modes - ([#5807](https://github.com/google/ExoPlayer/pull/5807)). -* Audio: - * Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). - * Add `SilenceMediaSource` that can be used to play silence of a given - duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). +* Audio: Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). -* Offline: - * Add Scheduler implementation which uses WorkManager. ### 2.10.2 ### +* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s + ([#5779](https://github.com/google/ExoPlayer/issues/5779)). +* Add `SilenceMediaSource` that can be used to play silence of a given + duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)). * Offline: * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after the preparation of the `DownloadHelper` failed @@ -40,8 +36,11 @@ ([#5784](https://github.com/google/ExoPlayer/issues/5784)). * Fix issue where playback controls were not kept visible on key presses ([#5963](https://github.com/google/ExoPlayer/issues/5963)). -* TTML: Fix bitmap rendering - ([#5633](https://github.com/google/ExoPlayer/pull/5633)). +* Subtitles: + * CEA-608: Handle XDS and TEXT modes + ([#5807](https://github.com/google/ExoPlayer/pull/5807)). + * TTML: Fix bitmap rendering + ([#5633](https://github.com/google/ExoPlayer/pull/5633)). * Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods to indicate whether a controller sent a play or only a prepare command. This allows to take advantage of decoder reuse with the MediaSessionConnector From 9ca6f60c3a1cd1642ee3e3a8e579184c6228cb07 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 3 Jun 2019 23:35:34 +0100 Subject: [PATCH 093/807] Preserve postBody in CacheDataSource when reading from upstream. Set appropriate Content-Type when posting clientAbrState proto in post body. PiperOrigin-RevId: 251322860 --- .../android/exoplayer2/upstream/cache/CacheDataSource.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 69bb99451e..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 @@ -137,6 +137,7 @@ public final class CacheDataSource implements DataSource { @Nullable private Uri uri; @Nullable private Uri actualUri; @HttpMethod private int httpMethod; + @Nullable private byte[] httpBody; private int flags; @Nullable private String key; private long readPosition; @@ -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; @@ -347,6 +349,7 @@ public final class CacheDataSource implements DataSource { uri = null; actualUri = null; httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; notifyBytesRead(); try { closeCurrentSource(); @@ -393,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); @@ -416,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 { From 44aa73147663fc91f29999c11ad73179ba73d053 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 4 Jun 2019 10:19:29 +0100 Subject: [PATCH 094/807] Use listener notification batching in CastPlayer PiperOrigin-RevId: 251399230 --- .../exoplayer2/ext/cast/CastPlayer.java | 107 +++++++++++++----- 1 file changed, 76 insertions(+), 31 deletions(-) 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..8f15fb8789 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. @@ -86,8 +89,10 @@ public final class CastPlayer extends BasePlayer { private final StatusListener statusListener; private final SeekResultCallback seekResultCallback; - // Listeners. - private final CopyOnWriteArraySet listeners; + // Listeners and notification. + private final CopyOnWriteArrayList listeners; + private final ArrayList notificationsBatch; + private final ArrayDeque ongoingNotificationsTasks; private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. @@ -113,7 +118,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); @@ -296,12 +303,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 @@ -348,14 +360,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 @@ -531,30 +542,31 @@ 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); - } + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION))); } if (updateTracksAndSelections()) { - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } maybeUpdateTimelineAndNotify(); + flushNotifications(); } private void maybeUpdateTimelineAndNotify() { @@ -562,9 +574,10 @@ 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, /* manifest= */ null, reason))); } } @@ -827,7 +840,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 +870,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); } } } From be88499615d478095eab35922782759b9724aeb1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 4 Jun 2019 14:20:51 +0100 Subject: [PATCH 095/807] Display last frame when seeking to end of stream. We currently don't display the last frame because the seek time is behind the last frame's timestamps and it's thus marked as decodeOnly. This case can be detected by checking whether all data sent to the codec is marked as decodeOnly at the time we read the end of stream signal. If so, we can re-enable the last frame. This should work for almost all cases because the end-of-stream signal is read in the same feedInputBuffer loop as the last frame and we therefore haven't released the last frame buffer yet. Issue:#2568 PiperOrigin-RevId: 251425870 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 5 ++- .../mediacodec/MediaCodecRenderer.java | 42 ++++++++++++++----- .../source/ProgressiveMediaPeriod.java | 2 +- .../video/MediaCodecVideoRenderer.java | 25 ++++++----- .../testutil/DebugRenderersFactory.java | 8 ++-- 6 files changed, 58 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f284376cfb..a345976c34 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. * Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, 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 fe8e898b06..17591a585e 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 @@ -691,7 +691,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 @@ -707,7 +708,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/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 6d0f9a4aad..d636467303 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 @@ -328,14 +328,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; @@ -600,6 +602,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++; @@ -706,10 +710,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 @@ -883,7 +890,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++; @@ -1010,6 +1018,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; } @@ -1082,6 +1095,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { formatQueue.add(presentationTimeUs, inputFormat); waitingForFirstSampleInFormat = false; } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); onQueueInputBuffer(buffer); @@ -1456,7 +1471,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); } @@ -1472,7 +1489,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } catch (IllegalStateException e) { processEndOfStream(); @@ -1492,7 +1510,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } @@ -1559,7 +1578,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. @@ -1572,7 +1593,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException; @@ -1652,7 +1674,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(); 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 dbf5f8aa5d..a56d14083e 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 @@ -738,7 +738,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; 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 7193c4c22b..33eb1095c3 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 @@ -712,7 +712,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 +722,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; - if (shouldSkip) { + if (isDecodeOnlyBuffer && !isLastBuffer) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -769,10 +770,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 +841,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 +894,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 +909,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; } /** 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 6bd4c8dd14..e1243d34ba 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 @@ -166,14 +166,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, @@ -183,7 +184,8 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { bufferIndex, bufferFlags, bufferPresentationTimeUs, - shouldSkip, + isDecodeOnlyBuffer, + isLastBuffer, format); } From e4feaa68f228a1fc4a35a4decef065252820c436 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 4 Jun 2019 18:03:32 +0100 Subject: [PATCH 096/807] Add VR player demo PiperOrigin-RevId: 251460113 --- RELEASENOTES.md | 1 + demos/gvr/README.md | 4 + demos/gvr/build.gradle | 59 +++++ demos/gvr/src/main/AndroidManifest.xml | 74 ++++++ .../exoplayer2/gvrdemo/PlayerActivity.java | 243 ++++++++++++++++++ .../gvrdemo/SampleChooserActivity.java | 133 ++++++++++ .../res/layout/sample_chooser_activity.xml | 25 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3394 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2184 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4886 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7492 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10801 bytes demos/gvr/src/main/res/values/strings.xml | 28 ++ .../exoplayer2/ext/gvr/GvrPlayerActivity.java | 5 +- settings.gradle | 2 + 15 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 demos/gvr/README.md create mode 100644 demos/gvr/build.gradle create mode 100644 demos/gvr/src/main/AndroidManifest.xml create mode 100644 demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java create mode 100644 demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java create mode 100644 demos/gvr/src/main/res/layout/sample_chooser_activity.xml create mode 100644 demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/gvr/src/main/res/values/strings.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a345976c34..e03a0d2dc9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,7 @@ for the underlying track was changing (e.g., at some period transitions). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). +* Add VR player demo. ### 2.10.2 ### diff --git a/demos/gvr/README.md b/demos/gvr/README.md new file mode 100644 index 0000000000..8cc52c5f10 --- /dev/null +++ b/demos/gvr/README.md @@ -0,0 +1,4 @@ +# ExoPlayer VR player demo # + +This folder contains a demo application that showcases 360 video playback using +ExoPlayer GVR extension. diff --git a/demos/gvr/build.gradle b/demos/gvr/build.gradle new file mode 100644 index 0000000000..457af80a8d --- /dev/null +++ b/demos/gvr/build.gradle @@ -0,0 +1,59 @@ +// 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.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 19 + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'extension-gvr') + implementation 'androidx.annotation:annotation:1.0.2' +} + +apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/gvr/src/main/AndroidManifest.xml b/demos/gvr/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8545787064 --- /dev/null +++ b/demos/gvr/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..bd9c85da51 --- /dev/null +++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.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.AdaptiveTrackSelection; +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(new AdaptiveTrackSelection.Factory()); + 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/demos/gvr/src/main/res/layout/sample_chooser_activity.xml b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml new file mode 100644 index 0000000000..ce520e70e4 --- /dev/null +++ b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml @@ -0,0 +1,25 @@ + + + + + + + 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 0000000000000000000000000000000000000000..adaa93220eb81c58e5c85874d1cf127a3188e419 GIT binary patch literal 3394 zcmV-I4ZZS-P)d>%xF$xW+t8d`u>^S&KV-GPLkW5 z`RC5^;-~lC!ku)^Ohlv?a=C@{LON&3(GBWZt}HiPtth81qyN7FauI`bxyAo)XVqgh zW3>@#CO*5}T%G@AK=NDHsZ^KMh4m_H1?vziij`JcTAI%)hQxiE_}?Ls_Z3mLuDVWX zS^p(KZspvdA!{OQJ1dz7PLR=Pv`VrZ>R@dXbv7*LzHZeSkZU=U@4#Bj$|wiLsE8zP zjUtt*CGx4WEBIUu40Ve(I+Sxi*XjmH{ms413S+6EC@KJ(K2_|i^|BRCO@`vH zvKRxd&J_Bf3hDvqqb-nZf%6zE79A0tLZWISqY6}PadGkEtSUjOQZP0c44yD!6&$LL zQuVDUFE6hb%j)25wdHW5UVK#tVXDp&eZ-Zrva)~XIwT%PQA|wC zQ!Fty+W@(UYYhZDmU+u5&ZUyjVE|K6yQh$naG3KcPKA`8pMU-Q`SZ0|VvIHd;(Glp z4aiFJDZnr(!;!a1$%=Mr;7(6Z58@*!eboU8^D8MSnPbIl1q%TL^9me9h6}%uu_((K zpbYG4bmPX2g)EbWH|9L8FS+t z0*O;;K|ujsym--*CB|+Zkc(LJgvwdVae#z^hLGX%63mDWSwto#Chopfz^>G_m}*;L zJ=$83e}78)aWGx+8%kFf4yEK@2Ac-8a||ihLy0$5b~^U>}5qrbp87E6|4$wR>8H{ z-0E||G#Tx+093fOKZSMxkjk~|C0{Dq-oMgv7iW9X*87K1+yF1i`T8p|BqiBpM$5>^ zNME^fWm8s}#bX8Q$;ru?Ago8gu^mwO8$$uYq@v#&Ql?DoM?)y+8To$()PE?Y4;e%m z!+j}#&1zB#@->CTlM?ZCKa(X`R0QOTK*jF0Rl1i}fDR7uF$?Hme;?$f)ISE(p6=2W zD*}?roHB_D_wLmsKFP82{w$CglsWWQKQhrBK76a_he)DxI2@lu8&9Ku?{_eZ)JXrrSQO{n8KV9UxGK;hVbaIS025Gg-0}>iKm~wym zkql{RHm!*;o9*7cyDv+(XU`rtmJT=n`Wy$USEPGs2MBI62$o?#KtdTKM^M4$O=K*p z(y@wy{n^>GXWvDrkxbO_$Ats*3S`zA5OigMmra1;`wsyGieZ|3 z)Nx4qnw~#&<`|fl9(8) z&Wd7VV~;S4+awnWNcZx~FW=oDvceq8@zk!FyRc3CIP2mM>r4fHNhF0-6De`*H|{w71NSqCG{Nv4b+kj;7)> zXUxxJoL+tP)ut>R*!&A9C@82U1IpG4P{yJ`w7-v!#W7~*W4;)OU6nCr6e)Rm=2p}> z-+%x8gRHWE0s;aaU_b@x4Qf?X7=46Jjq|3>t*i^e$J+bY9S~G>;)JPSD)Y zv&nnOB^ve1ar)BhE85s{xJ7`%9aE6|$;rv9>;>qP(LdR?)#zvW0y1l%9~ED^Sf#DTsZ*ymf`xjk0l8s$&HIqIK~ZZ! zGd~QWO)c!dSJ(m^_TBizj0*Vp%9esGu~-AoHW zzKhP&U!51;63%Zgzd=5WFIFh$v*-f7HETJoeqJB0_z#sL3=x^4F8}ljnP7;j|b|-}n27M*Mynd2Xri{jX7B z%nj6U4=KEZpuKzdZot^VfTfd?H;|mo?&6(nKNOM&_*wvxn*7y1dTst%dev_oP5kzN z$-E1;MADg?Ire-uj}0nbrlPmse%lLUm+_}U43e5Xd-m;|u@OIp-6gF^6F^pI!s@FO zcqxra6vZJgFPu1W;sB4?17m0D)RH$+ayGm7r=NcE=7)$XE3!YJVaqPlqQeQ4U7!xY zcC{Yt-syu6J{XNL|MABk?kt^J;-;*|jT?6d7ifIx(xq*f{#XWk1NtyHmSWQj9Qqob zh&_QAE5^*ym6yCx3-QPmM28O_-Wf-b!itRc05p4J1j;xLjB4iMFp@K1z<@3oE1&fBu~; zU1btC6~#&4sZ*zW`P!P_Ck|nAL<44B?Oa^0NiaT3FVQdi<0w6^82Oz62d+i?_U+rn zV{0SEsApN9V0L_|-K0sAC;82XC`*3wRLB-FW!<$(;k+FXO%W;iPW^EU!o`IP7ot5p zJf6karc9aQ&eGjV;>M)z;))uM9Xqx!@6&RK$n4aAH7Sp#sUS^TA5QxtGf8ps?=^^1 zbaZqs?-mAPOc)zWXA>Z3YBuEsxwf(#N4f5G!HhQ59K))sIyO)i}~ zcSgmnIW|q=!|iOb6Rwt!WI$%Yt#aOYR>D^yCj!x-MZD~YpYSCMh&=f zHyVvAv6V_vk7A^XO->b>OH|7jbYB1;n zFG;d~|NZyRxo?l7kD{-7mP3>N*=L`V-a`Z|I|dA!Z8J6H}*79TgZDxIQW>DhZEMgjFHQ zeVrnbk^hH3)KY(jyb8*YNT`dS0u$;+8Tp~%R{INnDEqfXQ@iqWGuDHw$NAfR!Ozcc($b|%zX}Zv-NcW1kML3u z{G>M=c{sLhTDEN2S15}*gtef~=4b=L)(+s114i6@?DP``ft zMmP%=;F_~q2sIa;KEFc)3NPzrep1o{^^JjiG{v$$j^J-@V^i z?tSUk&(8}0g3tz_h0tFK?d@&p*?!OJ!omBa1-tofC|W35I5WB*>ZWZl_4Q1F~p> z2qf?zUf=9~F@mUZ0&V3u&LYLAtrMXd>XY{gwi-c!0zW`-|1N?q762zeX3L`s`e>qO z@&95P!`0Y<0KXD}bArJ+A3(b_)|Z7zi|$wYO+xq}cDRq9bvA3(K!JCXpqc;QWHKG_ znS?MJU}s7Op4TrtNqf8)7U=EmJ%$j1`b0wLJlNTO0x#^;K2UQJm+g(JBpsan&jix3`b2=YOY?whyoAqienlse65;|} zg=H?K#52ObgK_q21hE$(&54n>SaC5F4jM>qSMDnA-SiB2NnpU4k&?=xWKz)7v0*6%)hNhmLp7Wlx z?~cG#*&N6mrEsufx!nGJ@97y(_e~6(c{2_k%t?T*6Mq0_H2UNVYPI@LfiDEv6NE;i z8IO$&dq!@GDj;!1hT>uw(rz?WA?6K}a=Z#Z9{saY_3XcPgY?hb+Gi`Drbm!(^{xEr}lc1%g3tnjY<)1@ONr~Ic+}he&w|)Ee;dTOL zWo55nqIl3(0v*Sq;f6vES)*OgeC4!QNr2s%VecN71lrr%)hQ_{VRizgrKMvrfoY`@ zFllO_YDFyMM8tX2yIL0G7XdPJQ=^Cg`l~J@BV(eSKw)9w_`VRRePDofE0my|yuc%Y z2Qvdep!q+S1X5E|Bkct8^72Mu5>rbha97<0@{}q_IB*NLCj0^?!xLQ+DF46@3C*4b zoktG4ln{n|&F%^Fi>ynd`Ul!JL198)!@|D2S&FA3cm_)Zy${7=H3#HkkJ^O`r{yZ>Vya2e1 z@fb!M8ykxe^I%~i7mTqr#TNo|j#R?YsKx#zvN^HPnVJHe!5}G-RaRF1j+jGi*jXtN zR8di}jIY1Fl@me&3w|nc3sxDq5Hi9NJQC>Gx&(63kqy*F?6X?sb<97?@o$BrHE zu>>3}ET@NJMOq-GoObqspJ!|p5%?iyr$d|7Uu8jB)Dlo9%!Tf}T({+dw{p6srlt_F zzUZ(O;QgSesAxI&sJ!(kBWD7NL*=jy!|Bnt7Nb8bfh|dUAnw-+hc@wPx8bO&2~7HS zUv^|x#AwBG2ePxXNj||2+e&_WHg)RMH*ksWdfQSu6Yyr+F5ZJitg>AiNpj~oTFzmYJyJRcG;3cY{-eyJy4@J3*s z@($!xwt~sb1?*S_wSNn-y-om6oH#MS)2ZCRUAuP4aD(5=juk~xs@1qA5SLm72UX3W z>oNu8U<;MNQRLBTwY{4+ZP`pGZ(|fXx> zj)7=nuUxru469We334imYER(3fb9hbjX|=(Xh<_Zjxg9Vt}hpRkVu4)4lzgcu;UqP zpoj>7+xlv2YwJl*Pv1#n`UPGnr^v|2=SYXRZ-}q1t}dn3pa>-(V(H^+C;pA54LJ2h zed2jf>6$fb5@{?Nv(K{eL{}#_??$T1T6|r6Xn-wpwSS^C~Hn~t= zN~Q8U?0YheL1R6UpCPaqOp_y$NYp}9s2#l3(SVF&qA2KL+F`6==^`kyys$3kdoH`0000< KMNUMnLSTZLL=Vyc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2101026c9fe1b48ab11a23fd440cf2f80af2afae GIT binary patch literal 4886 zcmV+x6Y1=UP)NZw)cXxMpfx5T2C&3}PVZZ;p-o4i2PAJ@j z+=Q8)wZ4!Axsm_BXYYOXxj|RrxHs+%BJu*>GCn21kMjaLPsdZ=m^Y{(+<2>%EO28%%wuqUDMu8OH$8%zKu7G4&}qFQk04YA1g*$9*T z-fAmCrB}H$Kq34SU>Gp7@Ek5m5M2v1I8?C_X8FB7VgIvuz5!mq7wBXR;06n?h|Qh^ zpAc-s4G;!&GQPme(+%+E*a@BkO92bdVTL>$4o@VHrSfOt+~Do02C(pgS3oS_4`l^Z zo>0t&81Rby&*~gy9`Es{e^8wD9OKI!Tp)18WVKp2jmH3M;1&be{m1d9 zjuM7eWu?2zR$)RxLBX-u*w`n5Q!~LS@l#V%#hHJEh}<7?G!99^gxuWRqXRF(=;(_=tajB`c+L4k@u+Sx9J@^Gl=@-gH zbtLBlx_R@aA5c{psY<%p+1bzG#bO+-^QBvxg}5s4BkSp3$(EDi>G?o{CL$EY9zA-r z3SiC3!~idWh;ewGB~Da4FkquP1DLI$BwbxbHcOsIw=OV5C|U`vyjeG=4PYlx_v#6Q zW1pr5FuUI*DgFSSOY+EkKo~hWIdtX9mA;@;qG0A=0pN&_+$2GE8k#f#5uZfT(GWp_6rYpFk-=oLzv*V}fW~yFWgb4PcD=P|8`jfTZkf%}Uawq@;u?Q>N4ay5-B4`v4syZWv*J zub3*RVMCPR$CU=e{Td|1QbqL0AaN|E&kCUZuju*rB^tn(-W*7IySeS{`Y?3x;K6=E zTb$e#fsPS33|Bwi=x0n)p8c*g;P&tk)dsAoZ4lKF&m7eTFy@GGD!6pfrR{2T5(xUv z7TPMSp+AJ5)~#FnB5s|~(txB1fh?Gls5gM~L)O^e$(mC3ZEx1__U+qeDpjiVAmA`m zGz4+=bNTY+`Z&M@v@!t6)1THi3UiwQjIpPIa+WV6DKD?g^FT^Uih0qZMJywMn(qh+O?~XvK&=*KE2U;{)V?3 zF$%RZAbUC68A6DLj#Q|19>{a*nYdzQV`uNEueJd~DO z+BhckhB^k?(?U-NTIp$56Fse|8Ae+h7$`L`&@ls;&|u1%GMOZ!(Ww@-NW^E)o?Qfu zeOYgyE0TaQDxw0~?hXUE`L?r*x>>NPu7NK9BZSfdiwXzf`FCZ4u}^QxUB8Y{(m2!| z#USL@z0mkUzI>&K0U|bqhK4?jHfkge(A)s?`~W%>&_k8@+Zq}u$v=oi(ggEIA5i{@ z6Atx-Pzjyy*s;Dj?}UiDUX}Q3Yw9WfbrxTH zGDnRhDLbpw1`r0WBaQy#lTWJse|NOV*5jHrYii@MNnAd;$^Z^G*MA76P0byP`NunQ za&;e>BS)wzMX|q(CdC=xy<^9YhIp{a)dnPv3#47$oU3?lzZC3N@fpkY>!mgz zIXT%papJ^QLTkGQY}vA<84k#Fl>u32w%qA@R(HOfGBDlMZItdC>mzvs$f5t~P-AVCXiQ+4&2H3^>xZ zhuaOvnlN5f5=NURB_blCd9enBhlf|eI^Y(UPOdb7=`s5}ef{ZTTGZq%b?xXH28wyb zO<^b`h_a_m6^k-eW$3M2x1zDusw1@KYlU>U;f0uYtY#JJ5>I+yG`; z_Xt%pVI3B5QQboSkdza!NJ|L_EHmLv@4Q9%`}aHAAr*3pUXW--JPEvUm4ra26F0&D z{1olzhmk1exXXavlTOgWX78}LszcKDMh4p56f>j#o!@P&YjD~GW0-+*mM$SFH`meq z0B*(+zBjNG8b2bmRyN~wVv|5LiZD%nOl%nafdzu)~bNU%tUaz0q4KNMtPkFm`lFe!@^DW7^xH!{i zpMBOlb<%JH2}wK6vtA(iO1-dz)`fadYCE$ z%%6Tt1*cDIa(yT+bR&F|{zIWH^1%H-$A}w7p7him_mC#m{pg?Cg1PNY# z`DHN+p2har68TB2T2w*51dT4W0V8^i7suFbYlMHfoE9{Hr-%W(xt={^I!R`;OGQ`I z|5g!xcJJQ37MeaMv;{gwz;_s7LPb3I@wIE$_7zXUVaU}6^u;1;kE!Rh z(|lYpRaGPn=|7p4*Xbh+po~F3lI7M-w_Ki%|0qLa$BunpXv*>5UJhV53_V!2YSr3^ zT^G17q{T{j;MhYntK(E&g7mY_QN$O^M1@0h+k7a_cpCreQ2H`rl?XAV z68_C4mudT@bh1j?imo{OZRo4fiWMuy3oV7FK&M39DB&>lXy3kl+hI+R#3iW)1~^67 zyo)sVXd-3im5^YyOTH<7etzAAmLGle(S1OtgzprAuMxt}N~cbpnrgkXGW2KzM(l{D z7^B<&mjowY8%|t89-SvNl(qEIsTsmCv2f_!yLTr{rDUE3o@T(OtFO_im`t||uRQzl zzamm`_WxaI*uQ`O`+?3L;>Iq*vCf@4KZ2iC_V3^SD|X7n3wVqH{TE%P4d+s+z^Y8J z?mzoQ<88^3C6k4AvW8wj{lL@_x!_4uL``s2*JWN8xYGdrTx{ALj3;A`R&iea#tXvs zwd&QYw-VZM?Oz;x$1cJ-Oe5i7<(V^QzMqwqMf}BWcNp-~mRocqHAibv)o#2Ku6+1H zXa)@*<+I}L?{wFJX#v?!HTPN=%$$@O+{6|>-FFE&H+fWBnZPd z#&T>M-D}(44PNMMW6jXnw!O9;INP>u1(|%heRnbt14b z%;Lo@rkE*HrZ6tZ?cTk6A5BrbgurH8$W7n8Nx<)a%hSQ~hu;gn{w};1_&F?ie(0fx zUWr5^1Hfwd@Zr|;X&s6kJa}-Lo{P^of zJw)x{Aa-#5_19krHil8cJ{~AWaV@1ohYsv(snrbMVg?%`n*RSiP4T)0OGht#?k~U} zvEl?QV@Ug|tcrm|EgWDaO&Gy75fbYi%YJ9S+CPZ$$ z@y2sCSo==oM$FWPHe2egZQ};S@251qB5eUAlC^2@Uh-&p%9~ z!k37Z{%eEZQZF00a_r^l)2APaZPUW*LBs^)q~fsU%&dzH{T4b zHc%6waF=?K>B_m@V8x0RYpbfNhGG-8brppTd`B_X4eL-*QBkDlDslAGQ%{|)t&h*> z4o#fk90C6nrmiJAp<5tASC#r_wKO{u8?eP}>R>ivYiKD}oLp8`mXn>GU7^?Y>FMeH zZ@&5FOP_e+iFfEJ;Rq29_`#g9$6`D>pbHAIh^{@H?`T@#a&$&_Y`_+5GTYh&xsAS^ z%FD}(tu{o43>h+R@#4id$PsT-%2;cL?7vtbN}^xTks(4RviXdzB|2#p>qf0$O+Z(4 zMt5xR8*==Iq~zS(+$^mS(LmRTqy75z8>>=wk*fKVv=KIvcFy<_qI5#eDR7g3Hn4x6 zmkTlOo`1TP=y2V2*InGFPoMGVhK}fp&gdTQkkf>bv$%t*bB-G4a&tsP!}z`n7cM+e zy9ZWj1JkPj!lH(&8IAzXy0kz1iGDQxl}JO{--GQ+I*w+XB3Z8>6WP+CAG)9ux}hVw zdKuYx0v2&c~z1(GLVH#Wc!ct;U{-SIo<&2pwgMf zfoA&Nga@l36&A8;0Mxk7vAwUcG!^`Y-&!8ICaE@0ri|jx?k-uv5rmFW@bA2pnr1XB_`0cAr~1`(4QCXBG>Tj>W)rr~m)}07*qo IM6N<$f{P}YasU7T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..223ec8bd1132aab759469bd5ad2232f35f7be2c2 GIT binary patch literal 7492 zcmV-K9lPR*P)7bFjo3yR6hYmf3aeOcP}{a`bNw*d|0U;cCTESAhdF=p<#BND ze(#)nSIgQ+Boc{4BJn&=2L@r*gF)y(i#kyntnL#ek1(4lg3iwuHMt@n4EL&P$1W2%4KN`sv1kd9&jD}1l8iYZj<4mYg_}n>DmK;k!doC zK-8Ytd#%B2upQh1F)j+>AQoX0DsDZf*n~wmwTKm?d)ZF+)|%ZjwK)+eUDYNJOanW? zn;L|%l_)+zyns-GRb8`&D)ol$bt=dUuPTW^d~&;k)!;FcCKVEfWJxcs63RuGP>*R0 z-T9A11PV@^$>1O;W0l6DU%!{(q++pFS4Lx++;Sp`K)hAW0H;{BQI*EaQfwuY3XO3l zp9c}B;pl8_FcBQ(;;8nNBe+~78uKK!*3(6xI$+}T00;fvkT_nBW3h$OLC}NEkqA^r z)zLAD%kfz3Gl$w#bYMHUVww}3RU;9I_4i5OP5`YcS6bU$*4E6Z3zQEuVm(mt@2CU> z5?Q|aSL4y#5NK~Mg#jwlFZsOPqA&&6XANTp%zN^pJEs3?(0 z%mW&rxy@aHc+cM0GF3;ak!4w%G)>b%=w(ATBu}!%CsIgy2-ZR` zK7IPs8-!AcLXii98jQzpp~)i>ef#|R^Dq#&M1i&~l4lSef75~|BzZ(4RQx~h-n|uaCmfD&_xA;bbBL6fpgL3ffTX{bTL=(&!o5T$ReRGIh+p* z&-^E#W(q)kq$}0MM!=PkkpYpBks35%nVB%rjB%CERL7$UcYC{>eJ0L15>N5JQj0ins zbiIe*L6Nr7Xx+mxDP#))AkHpoK=;T4^ipoAgNS^TnVCtU?BC=&W9vgki{|X??B0Cc zqdpUw{8)5QUN`)lC(a9fDlYz29zjRfg}J?J8x zP#@)}T?^@O1Qzbzy<4P=%;35YimWD_x{u-jirH?7pg4eAbH4e1=l}_OERElh{3n7g zI!N_(q36$^r|#dszb*OP+1a@aniguRYLgZGKH%@q^XN-kNV2ttj2(_3B_^sa zjG>>Yuy_xBUXgrXSdYx0sxuav2FSY)6VHkSDD~Jd@NBQ)fWo)a-8GQuJk(%tQVtJ; zprQ8pfRzEV8b$`mBZot_rw3q)qb4y;?F~3{=FFGmdlJ*d7SXhYQCf%P?v4ELr3`?s zY3#^}M%mpdwuOG3Y`39sXOb<3J_c*S05MCMF&%QRT~$T>2$v2=Uuo21H~GG>Fji>z z+OcCtRs6&cnRr$zK-um?!LyylXn@Z2&_J%w>$AE?KU(A8Dd1x{05MBjwF0!ypDA8U zlODobJ|f1CA78t$Fji>z!rH(``N9_{6`+^DSdjyTSbPClS*)WaWo-P91AvyAwkCNKpWx-{HZ72RD#`O1v<}D;`>egoL!iPnO2AFGcS_^B6$F zO%OmFv$llv4eLRQic(ZI_UzfS4Aj6qi3KVu8eB+Z*%zjB4uFeYL#JSXsAZvAfS4uQ z+d|f{qoB*oP(o2rkL4RUckbLIVgb1*Zzw=a|CGZxxCwCSa48+2EZ3owR;wByW+@ZK zL5{y4=q2fmhA~R#gM)+j5eu)=K?Uj`TU%Soz$##i-%lj~kx|psBmiQTwqz0Hg}$-d zMo0$%%9vo%LBvEsI_S0fXXD0=b&;DFvi{Ml0icwltP*Vk05P+%f{e|ZK#Grl6U|jR zfEYb|`0%E3hn|cJ5DQ;ru&qz8yVi09v?6<9ROoYSVLAXYOa9IQvRs@&my`2P=^zr) zbaqTSc<|uAh>1OW_LM_2%Ww2Ueq#cDS=h)`5V9p!e(`Q6w-NOZ(w_&^~%pq5#p}fmaoRudnYqV!{xB$jXy|#-lqs zn1EIzK-V0^3F@;Q93b>V8%VIUEYO(=R+bPu)JiZU1{jTv?Ai&sq@)s^QpBrZjuI0F z0kmSp3erH8@B_Wn3Q+uROYrI_Mob6et`g6{QK%)48Ufi}o}kam6P}6X;pe=5{W=ZX zS0*;l%<>!k@IUt9haVC^mGO$+Ye=A~LKw~R+!|vG?ybeZbG0`MN$RlFnK(9bSSgg9G#8Qh5LF_K;|)WMDXL zku^WlG^7xKhym=0J!;snVLf7l7ZK%r4qS+YjkX)Xwe|3~02LLWt(!&cKw3ipVnxIXM~)onj__nF1)v=P zufX=uLzuF`ANG8-w8SBL{*~zfl;!4XG(gxF%~`s1Xb<^Kf(X!q{j zo$(W-vn)(005Myz#2t>kJGI0By7w>B5Ybc@LKP1p-g9YzZW$iJJtX@oDw?LyM{$B!Q;jT<+vN1*_*NyM>Z$2P+r$O|O{ z#BA)gE3jqQI&f_%HamOrg}TF7+VW+f%ghu4P-tjq^p{_L*@4)gb_SXmGqe1^U;v@j z!Ex45*2+*aK+LAE@r46}76=3Ad`Ab?L{*pB+d}5ygP_aK7S_tZ;lfAAL?dE@7(p{* zW|sdKxGY+>Y*`VXwXqw2MNI&a*&TcUOXmFsC%R7(R*_DyQV&GwKd%Dq^XEdxS|v%M zbMhg`L_L`e3p36#$DN4&QZ0NdkS81GPzylJM((@;$9qi?0+C-=dx##a?80K|tQpjW zFR{B5FI~FiOPh+!1|J~QLY47Z6VFEPR4YJa8|*g=3uj)~Ho1w@hf%KqiGH6nQ-b9i zm;>_m0zEppBqkEs(Vnws&z>MAWH!vX0L`5{w-P>UU@CsCQhk$%oRL;~tB;eD(=WtCQ2?rrV~iRAP*fq&FnI1$ z)%uN{%6RgXt@>u6IYagMP84u-beu{|7zEJFnKLO1tG0LV-hTLrI1N>00<>oAE+w4{ zQ~qfTeifQ(K)H(-FaCm<7&B%}#X({RZ3aFt4xCi4XWU?0p8f@D+8oc13)`J|G{*Q zHq(jJEt^4#i+hu|*HZ=-Of{g!jT=`cHqZ)P7FZLiiqE=FpFZ6T0IC3JjN>)jeZDgZ zfKnF=I1?`*1H%mj+}+&|5(_dD76t({c<^ANt+v0v|0n=Zg$7!<$W?LIyD0xw4!(fK zW6iK_+qPN60x@AIK;~b4^%V)}8uRAO>m^B266=X68KBjZ_Nf-2WCuIQI)04xu9RSU z0HD`lrj8gfVj!^~7ZJ@30*J&??b@}g!{w6jeaPyAc2nSL~%^s(0_+)tP`cbXk1<`R7$gJRLf8 zXfP>qRu57tK(khy6$21fl_&jr>Ho(@wqAy_!^5{_txt*0?|9SN2k#70& z<->^q3|~w@8@$Z%bSmkfnrJOCjNN6skWvBqZr>en?li_|fR4R44eSm*cd@NC)B3)=f$Wh;04vtwsa% zm?higV zY;U+8qFrk`Y8(1kV{GB-vkfy5mc4iOIE<|a)(g9*N@z+0L<%GRJA!h%; z1+aI}LfAHVHT*Q~FpR_0+}K@bk@sV`AC;jVP&mD>goFgD{Wy`Yu>)V0IOehbm~sIsd7JQC1Y8VD085yUbr`-wLt-g?X#!}S?{j#OEMTxpf$vGZfB(J?r%{KKk2`kkm|r{#0f?HfLrg-hc$;j}ghK5u#m^g9bw|EI- zHhp7yS}Fm^{_sQCcIhR=r?d7$sWDpg{)-%qMq^DrG$8X5z6+H@tKJe9K0CO&x~{`A zIFYRifR_3^fv_0i1MI4q_rbRi!uIXkw~+5-A66%y@^>HcyHG{6+BIs_Xob(t_>%hH zvCUB;S=qkeC?ViXc8UQ1$YjvzOdl3bzl&ZV7f3#neOH@&Y2fZ7UQpMqTbH)KHr={) z>x*rUS2Rx|8luzp6O009#IZ0qaU&KobEySWdf~x0Fwsei=uVwF4I-bR?-~me&PE98 z8Z4BpT)A>AruKT#dMG+Imo!VU#w>opF^i~FHMSqbpYy_L3O(MwGL3wNx~CQSPMB~u zB9v7^tAkbQwzMstIC0_^x)S6sTq3K~y8??A#uj)zf;+F$OFKBM)Baa@csQLh-A+D| zeN~5iCnj_W2xVyXt5>g1-&j|?(aIS|V13I)*@h1>BMQS+ zwe$S492@H$$F`jmV%ufMwr$(CZQHgz_`lTOsaH2nrfvrJR-M(`*Es#pf6%A6<(9na0X9wtD`8XH2P<^wY4pL^UXIuLj4I79oj;)BvePII@3j# z`3coDFKX~ibHr7QZdHsF5aI1tsfK8}exH*{rFyTq=9*iTYy?C+eTiT6-&n^A%4?7s8mBq_$Smy%R4Z-Hgl21k-3lu&q87y`U4Q=fAWBS@gtzGo z=o+5Oq}v{JY+YA}a6Jm1kqV+JC$VN}qFs8a-65O=`1$oa*@WcS3qNF05&9>;uv^n~7=PtYKvW04^-z00Tr!O&Sf7@jB;Y;3lcK-L~3cF2<86jNd0JIGk*(uC4||9pou=mFz9lsBl#1&qMtrMCW%^J#Ym1 zcI{y=jt(0d8rspRbz6wf=ZT+*m{{H$c?|;5Z1!jHi?q>q;KL6;{59EAJw#{`HcA|2 z(jZEQe0|`72Of&f(4Bt}8?4j(umDdC$;O;ylB8C5&`h5+X;KF?yylu~u71T8SKO@& zjs~#rb`k;7m<0|xfSF+(1t0gWyzJaF&X{ztqd`P-^gxV5HH^I6q;-2`sb)^yLXBoN$g3f{UXQ zbR!+jL|4|Co#kN;o*4Y1oOg+ITl3dYk=m6ye)~O#l?Tf|Hel4GXddFLH+I^XDa)a!KIW&;DK6CI!nvrf5nl!<|LMl>5u zuzhcYvZX|lWL+o~C-UTzPd@98JMMU!Y__hN0Yyk3VlWL?w&^$9^nreosBA$n7ro%` z?3GqpX>9|ii#gj|HCY!E64v==zEQ!^MKww+7K^ipJOU$0!3*!V-+p^5YI{)T?H5+k ztI7v>45nb_0M!2tn4be^I2i%k=!%?g82jFP@4Zh&Ma3e4ahM}>HE^n}gTg=ebrLoz zGgTK|2vs9#HX{Yo)-9QP?X}llSMT*a;kxUtdqO7g>2GBhD7@b2pt91t8I%0DjWx!3J=QM9&K#Ia3F8FdWE0>FJ&;gp(X?J^$FfzFKbxQ6 zbUGbai3RFyZ*N~tH?Hq=+;PX9sN>hv`M)f4_XP`!$s^(cSeO;S1*`n&V@Bcsa)6TW za=RDoqL&%1!x= zlC}V*z)Yopk}RdXVIiIOw;4|Go@`-@=;R6pm@U}L8U)81S&s=rYRrj5L{`uF?h7zCq( zUjZ=QhfbiBYxfHY2$Dq*DwC17fhw8%8fkW<>u;^}w$O%YH@%ntKG+B`gGs< z1T-TTOyj(0DLbb*Nx@7oJ^;%MQSQaBG;35xq{^RF`2S7`Kyh0|x1DoeDfc~Ylz=7y zR0LE=%FK{4OXMv1KaiXy>+hrkP$FPS05c{)6#-X7#ziFhITCGW%sqoSto+VsHI1DB O0000?3S7wcyyOM-n-02&+!#EIxIce+u1iBJ_{ZA{D_@u zcDyU!&S*>E@jvsdP}X7OsVqtzrcRKo6p#%(5zG1PuE3<= zA`eXEIYz+nMzu5Nv5#1B0mMV61gJgU$3uY&2C4%-{OZX9z2jO;A z)(PKVr@6=i#BjkurqDkA{#R>AS4A$iGyhO_NMg^dSD)p|oK4QcspI_YP>r^*N z3b&fROcF~|Hj+o*^CBOj#tYu%aH(1amHBswiR^;-Nj#3)SOY8fIc&ArMN=|p@yu{r_2BZ?Or-rRVji`=Ib%+v< z;*cH7+w_z*3JRvf)}LMwM*}~eM;M_Dq#SlTzto*^p{WCIe?7{ryVBlwfdhxa)=7nW z_XK)_SH#hvC@&0Npp{A5&lhr3C^V}$*ZTu_3%_A(ENcDLj}1bEJQJZ5#Y>0scHaj_ z_d6p(=sXaZ0mWy_xYbfvVoAkHI7DT#)#i#U8Pcn}n;RJjd9GQSVCwRm!MJa^ zA_PnC<^*qF(!}xP@UZPIW~F14JyswE)=-79U^+8sr|mSvspCQxIQKTFal^^c(I|rd zLS!qoj60ah&k)=oai)?l>@A|J2i6WRaBvmc?RGn-9?VH&oQ%=-^t@ZU+M8IznIi~t z$7){SyR*!vehy#>y|DhFJ7dLR&~gsYy_p=XFjrRgiMOnhc)vW;ApF&MeLzN4!_d&5 zrc#ihks9`JzcI%=>TrPBjSoCgB}H^bA|nlm)cL0qFzE6%^wrJCbNHjmVT^dQR9=JR zo^(OyDr%iBpkDySsCZwm1J1>CAI}rI8*M>}&SC+O2l%`cd#|u;OjzMGa9NG#dYcKx z6xwe%IXSIG%*Wtj1zLw(TKkuF1X60QRyPA)MJi!}#^r@S1$(IEain=Bd@C50GM!ah zqYHEQVAHsr8}34>{2P`#0Riz}#Y9`JjdROjf2uD%m+;Vnmx-o9KL-W|h|LFL|Dk@M zEoXy(a!X4~8(YuC@Gt^><1Pa#qeDaiYX;>o@3o)MJ6yC7d59#DM=tVD|I!1Ab9}C?WzDw zLS4nH(`DjN9BYiRjQQ+!P;*j^(bRBsRZCnhiPvy>WyO6(&ohG!Rn1S&Af)|W6)Ri1 zWk`FwK7dpMlwNM}>-I@~5%10lc@$!Dg6p_2zb}jdx`EBG|_Tz63 z2I5G&5m+~~nMJV0Q-uPlFa8cMrT9eD-F^TKO;92W5BXX0>>(c@natK}&@z{fWW2kZ zV_}}F*u3Jyp+|Mik$sgSuF(e?Ee+HfS#+H!atf0aHhPPi>=O3(_tP7NKJEl&icI`I z(YyS`ElU{_Y@+pMh*e`>{gj=KWWA9Kl|tA{T&K$5L5Bts=_dvGcE3Jqp)=;*oeLOz z0{u7~EW)&iUmUZXWl}t{4LHw0UWMW>n*LP&%kn-^ogyAm0Gua-A^cjhI01ywVT1*VjLa66 z>lhL(>KsKlqW&1q>}JXk_4o+Di3l_;UNP;{zR1-oI-0LG{@3K7Z%7?>nG5cHd?QcU zBruWY!m|t|iw*Q6!-Bu_x_S z$xV$7>3UNYKSaYs%={Nh#3vtwroS&?_XleOy}#$FAd$2yLu9UhFJVym$PJOCRNN?(%^x z_PU_MyiW6uNjo2lqyVDVyv=@t?KF!5+V`4WFt$qI3n!o7BPkd=Xu`2;%R2Q_feRWH z;d*o;BPR;mm(Pf}DzB(`45=n3fwEQ47@&nk50NAX0)F)MkznH?L?$-2o`Uy|95Q%l;Z&Shi0u6kR_+k~uFOB;k4QiZ0&2ct9P?V98Wg23Q>f|l z&oR!fW*M-9YEKZImyr5`IN{~PpS;`V%R<9sB$tQ~fi0bO;*6*xdvqtUyNV-c~TfqOcq^pV9zyLtX^G~dI(l3PDHt84ESjUJ|G zfmANRK5Z!L`UFea-m2Tqgu}}vW&zj&d3C2UOIOS$yRCSaS6BL&N-Td6@NHic6BFBD zKu39%Kto(A|D}oTt9n4gCCc%>0-EYzAy1${EnjHA;{6~&Ped{|*s z3AmAkoKsf;GETmA2^kg~RuWwYr>dpo*`Dch3o_6`BEOqTG4ara>QXIfaK{E^6~~Sx zLz!y6T+!(YKYP2*=Zv;lB*6~j0Eusx^idN3aol!oDBJ1>XoFvTW5^Enb?PRr*+7xb zwo+KT zd1y*@>p6kNH`}jdux3%5*F~^~7cKwop->)63Vj|HjRAoo<&Xd6{Z%2e4pd8bOfe1M zoPN04^Ve_uqJ^nzIZA^DtA+-?Sff0+-W<+0?_A0`fg`i#W!cYT3;<&dWMX||#N$-a z_xuwo02~K0X76L7;WjU|Y7yWuzJHBbfAmBcdR-bAcdVKw*B%{r9NA(W?yr3Sp%yBn zh5Ph?o9Ts8?!yIwJk3j094gfW)?1iNX^TLoLq-5f2-z8SHnm~7 z^70a)hEb=9q{kKr0)a>_2;0i7LAf4|KR*DmpH@A}-iR2qh^-GB(FmS5{^#Tuf9AeB zrLK25pp*r=!MnKr|JZ@0qGe+LWkdp@^+U3X2rhK~6E;f}QY*!+DrfO;++z-j&=%W-A+-HxhRhgG$H^Emm! z4zf3w3?qQSo(_a>K5>KH`ZCW0{I>UDY8QzD2}ltG#`1i0npH51ODeO(=0x+{&lmA6 z1+?Razxk?rCy8-0ts)i%X6Zpa7n6KF$^#IY2+l4F0P_i=Rsu?{`yx!*$LqbtB_%<} zQ>5@NAkx)NM!*jae_Rj28vY9M`)h;(@v5ofwI4(RevRD5>0{zy!+(%4q6OR{nfjZp z$Gq!`RJ_HddGOjtXYowu4bZ@R63N&4zEl5zX=EB;4Iv06aJ(wxk^{A=#);`su56hGGU~CC z{<2g<(R9@D7rQUh;&3hS^_iSASAzyN3ciPbu=j|)?e@MZ&GCax<)c6Z*2ZKsRGq9< zCNJi_V=tw2qVJc(I0CnnGrByA*HsMmua_Jfbn4`t_K1TLRSX-zZHP@r6sa3qAuy_W zR*8KSr`3*DWX{pK$mBVO-e3T|7tVqB!zQaQt@u5aL$Fxf5PgyMp45c?aTkSN0a@N@M0$2U6K5YX^S{pmG>7cjL z5DN-9;gjuBmiuOxdBMv!)&SY&q{&*55{EU}j&5m1P-aR>3Kvc58zQ9iUo?OO(YLa< zqiW%vuax+xH6qvmJGovjPAgxYts8dO3V8vJ9WOW&pnab@Ca4m3LgQE zK;O#+4I;dH(}W^4s1?iZc%uvyL_*mN@cUI=)~@p}=6+H)pB>Rl^sR-+hbXt^{C zXQLurlDOIJ_V^zwOxy{?rmqCBJqzT570blD<=x?LE7AddKWDIhZE`#Y%r*rCKK$eZ zp?iD+Y1mh0OI0pA<%w7kpi~PoJOenm73sc82c2EzOzkFY}IOC-PPOJJmaX zs&UFkIy7maC5jn;t;Vu%N@l@{FJGA%0Dj^J4_SbOxi?LOAH?pqcJIPrNphD%I2G8C z$QmYA^WD*`HP8qGGA5fX7x4R>3$qdTHBN{aYd0>{W^<3^-d@x~ZwQ}fE;{P<{i19d z3Iq@O9v-Qb5X*oc8Fi_EFfj1;WCK4D!*aX=5fj<`8(WvKrRRaGn--G#Oa+8PUMDaT4 zqeo-1$udL1fL393kmadMT3t3k7~}SBw`nj13e~r1P+<0sXa*nemkd_$x&Po8plg)t zOLUZW3}7FDE{>RI*YzjrWUK3*9B!mgPxWQa3Iojwwz%|!o}A4^MzL+mfaTQ6@l!X} zQN09Up+xHleD&rj=oE{Hb6v`W*i&Ek6F^inyPlXsAptDj@$>VvP@+X>wS4?7*Q>ss>|Ec^n{SFZ9LCHSbsc zV|K1?EPf6S+&AG~>8`FIj;CDSK05M!7YEvQ81d* zd>8_7adG+CFP+(m4cNF?C=hzwhkWMrH(%zS5mfB&Bu(`r5n6)0K$BxQZ*r=wYSf+J1R^UDD;mQ4Up z2~qT(14Dz$)_~=I=)?|0Yqp!mtWQnr?d=VZY-GBXho~WF+a(h@?EoKmatWpVmly`U zgLEVK@Gta8+g`|Dj3{OvC>&D{J7Zae|FpKf4ZG8uy4)El1N0dHS+JD!WdJX70(>lw z_xBAcC{%CF{}BMK+Q*U*wCc$v7`+73U!P9hUhuF!JS8DNbQ~xk8(jm`F0C{$uDUeJ zRNo@J!>W`Y9J|2!H@8Me?d3}Ru^Z#u@a(yALmIF15H$vKw9#Vzg~ ztQ5|7d;amyJNs9_4MHfqx*GlRdwqR9b)zrh6&>9bwIQ|<%yhl#dsgJ4+NGGmG1f}x z{7EzTI1FyWzBCSFiDaqBH8ND0oY`Ryb6C6PN^U2}>l*N_`}O5%9%5PrKqU?BZx2G_ zwf+0Or9q>_N?rZ0S`@&_mr8shOU_2BUU|UhnoL-@JcckyP*ikc70+WG8Uz?{6ftR$ z2tj(w*JA<3_6@opJnXQZQcML`ePmG*xMj(&Wd`=#IT zWwwGIS{b^sd5QaDHmwj#3|2!^cR()W(O`L;BTZ2#2V!&1w9f!T{5O(532H3^bKgnX zhw!LmahG6mj&xQkq-#OG{s4dcD6N%`>l-IBM}sxV3}mncEz%dn6;4UbPiIQl{W(f8 zZIuNqq-t1=(P=0tts=LCu}=Lbw8bE|i2|(Bv>+6Up+!Z*aIDMPN~qGeZ3` zh$}(@r!tC%R#Qk_9?o9&`Dc&%8lEfH(lR1PJ@)v|e*mGEJ3r1c-k%m^qU+PlmPU5 zi_cbQjmQ^UV(21~WACWu;Q+pdmdL{sp7omp=NM7sCM_@JWvNw4ufAf0Ue%YBpe0ydjsS?@(l(7+~E=ve%BACh{G{7uJR?^o9&77Puh52 z(Gx7I{9Aw0faq?T*Udio4;4gopk}{i)8!;3N~`(&7xe7pLP7NiY?O^9x?pySPFx`= zeg|^XbxWV?SX`M~kLba49f)2oCx46dceLrs#ykb{Ee=(Y2C~9Zt)aoEiaYT;Lc+~{ z&*Oy)IgYvXA6slxSYZ?jk%lM_7sbFfSM1sZSAe_v6U?fj6v#m0cqbY;6VFR>Zww;#q zWqqp#eE$hAXCO_5u8uI(4WbO63zj3~Rr|e8B+<%!YtdXoTc|W__pr3wQF&v5E%$GC zEN4C#eN#yHHB}jyhYP@oo{~=r{Y_;oUpEZO&7W$zIVDQ zz3A+GK=qd(SZ_8amas!OQ~eTd2r_o`$9@viX!AHZL|5XoU#MyO`e1I#`vcSscLZW7 z@5b|y{sTS~nL8*6uJT1T8A)sl+G3@cQjfjbqCG#~5XINNpSMN&wEuv0lNAuToF4WA zBV;+DXsKFV^%{YDN$`9}B`}eSlatf;dVi`jsQ(d;q1%)M@4NBe_YbGBMp(39 z%JJmm2{7b=tU>@yagIkEe}f{uhX48o|17+UnT;@bytYPf0=zX1Na63Ep@y&#JI+-S z&nfeNt~HZU5k<7u#pOkrbd(YBep)TvZm=evi!FOa^r)-jcVOZi%3J#Y{SG=yxU|S5 z)6uQL@96E*M4pTfi( z>|9ush4g2bAA+OEazQX|6cbZ7J-m-XQl}cS=|xxU-j9R7e-~c5%KALcr*P_i?%d{& z80VBjJnfJOiaqIjq?Hq(5JFnUhW;e zYWg3fwR&p*x_{0e;ap_}pj3-khRRtQuwtwy^8;Uwo6zL=n z&_u%s&txIhLGP;D|B3JWL2>P zlf_221v+Qk{UpHTL;XT2S)R?v(JRJ5pPj(L?pPjhKZtAfM&oWt7o)A zNqM$bg{Ez|BRS8#irbku;Fklgxo*Ty)EDZd>dCNu{JyTj;A_(x2ruI+Kngeg3rPP^ zKvYXUvSbU55=NkGrY!#eDOI=WiL%jxtfe^&KlN^5r!5)y zSF<|C{#eKIL=~7y<;nLIiP$40s_X&0gIs*@MWKAccScHE8QKp35*4q{OJJA=ra{>c zfxgnp(o?y!{riip17RmoqRgZk4Mh(BnNNONiIQaQEg!?n^D-6&xex|Plg)lTBFJ$t zBp$f})?7y{FS{KVQfHCGd}QIm*n8{oTrmg%Dv$e2hA4{ywHiUNOSx{X*?qXtZjP1q zT8m4F5(xz))E!*q$9GnOSal5Ex<5$zH?k8!_FRVz0q>{3ct=kA{|Z zfeG55aLz*|0u zaGXr@h4 zA3?u6Q_(++HEkOTM>Pt)kM}E1j_pY`0Ej*HPabsM{$VWeLspup)T=Lgfx9L?xu*Fv?ncng8 zk)Yt7(`6x#6zSe~g^~TWCaRP3xm??^^As9_u8F5#4<9~5<3){L2UmFkddc9atHq|y ztwskP=PrFXSNs1t#`V%QuhyzIWfxyT`$h0@dqhW1&qs|d$HH$z6BU=PCKrI9Ylw)W&czkeTg3QJ%wQN_t_+Gqo%CCvMg)?x1p1=SuZl zsQdO%((@GE4n3(Abu$O2T0xsy@rbumhUZ$Be}rnp481!`<{Hs!d!9I&x$^UHY$?+S z>SD9g8bB(ks+L3Ch3xAQDCExDl$Viz%_cgctr)Xh@lX@Z$Q*X|E;fwj6AraxC^|Hj z1N*z;3+zFW!>Ge60y9cdnGar7C#X*L&Nsb{~f~ zWR=BhDoLO`j$N^2BfKDZo9g&?eGG}=@X83^(UDrp_gh9_NAHvRajC`!CGorf;aS^U zagW%oaz%l^AgK>gcr(BB?~KWEgrWv}YFU?!`2Vf9%lQYYsHh-6-V0X2-n*&Cv=zmt zR{dpJ=`ZFGb4VEx2)f3U(cq!_b(9IAuXnztskQtrpY+wl=>DrVH?uT%!)mCcNVq$L!6g-0xy4R2JK-Q5{`I# zQ~xEA5$ImRBQkK`oy(BcM%5-x)6Z-@SuatpUczEB?{WcW8OE3AB5AUS0ok-f#e2A| zK{n3oX^vyUPUWXoh`e*q#Q+8aAQ1T{7c&ePW@7k}ujjCcZ8IL8Ln*B!;n|zNW z3u#Mn=cCy7eRcu+t-*m0A9@)r!RGhC_kNpx%2r5?tr#n9jm-MPnRTV3%)TWdp$d#@ z7^|}??5cEQ0j&+%5sqsyj{9AT@*JM{lp#rAT2!cW|Zno zx~W;Cp)<&_W&!~Zuc^!$>BYJMk#>gU!r{!<%o)p4r0H?_XFr5Qv+6aaUyTU_`2~Jfkm-%@yuMhsg zMMiL)iMC!4tGFjp6{p4Z+k!tf6N@Nk6p(WCy>N75ZeYCt>k|5!)&|TiwItbA*wpLz z+UlIN`wxDc5N`i-B)(y9XE|-io15ZPB3wz(euy4 zM=AonmQ$bT{d6pPc_HBrObA?l!{OYxee=!%enQJ!YGT{mGTqE19NF5GbZM~=%UKj6 zs0D$?CVChe0n0pmkv<>Vo%5SmS3u1B@^|n@KO5CsbZXX?*cFP6v?=}9@q9l{!S_Cr zxl3^AdJR72npbVKe-_I4>I?^S7uh#N7HIOoY!phn!zjWBp)~EZ^0M+p=k`eW!{fhO zKKC4tYr(fLpc!5$!83OP1|kDM%=RC1fBQ!>MiyqtcBvtAPxPlp&8?kau(N$CYksQD-YlPbh4vhd%Z z64NGbW_#Fl6DMic`pu(Q%Zx3eHZeuz6(qI=0V2o-HCJcQ$VB&EQkY#Ik|XYZ*Ws40 zvcP?JOxooV`XjoTAtgz5J;?%*1PLRwMtsQtFX9}!@hlk#yBJ)Htq?fnC!OFE=odUr z6u7NCS&i>5+JN!eT8Y=!WW7ye0(#)#368n!w1VN}?S&w~1P z+1Iq{P>SeDFu>^r9j3?mWt;NF-u}(h2F6;@*|%8HdTC#bUApgVsY>6%{rmXisru_C z%Y3xuON9`)YN2$UR+Btn3``#OD?Gj+D+j>#41)hf@f2T8;uKMY*DX*s&%zA}vm0rv zGYb7Hp-?4O&A)5x^F!*_Sdr-V`@0BN73^m5YD*l3dwn1mwI z+gQL;V2|m%-X`JHq+P&LvKXBA(Yx*pYf&H>5Hy&(PuB7pH=4g~Bao)X{`KVQ>6!8R z+LXSXn4H?yoH(jspF+&h4tikg{u8)K+l4E|=GzXu`QCI5oz7{zW)M-RzFy`@>FRB? zq|CYJ4P+lwbct3=_w-S(AmmK+*(i$Hw3X;UvhCXE{~ip)|dX+qill&JfM zx|ObMJAB`+oVn4xSHE}KE^hS==E1~C1G2=U00BkhMm7QUSbk;jk2-}0FYQ(Hej>>M zuHU)C(y~;njtvB)Z=MaGsqPKFCfX^TQaAd{I8G^Qp{i8d`gAvaPq3xD{mhknAQFNT znvd;07tPHoi^0uDZAF^w5hst2@aCEPI`SZ~SY{lFmt}_SZ-}xNeaGpwwZvo1U33o~ zyo^pI0V+!7?f^G3*PFx!uqh%z2+I$mAI4D&pw_S3${MT^C#-qr6dNJeXHD24tSH9# zgo$eXE*KxR=sqT1_Pi#N8nFK936t|kIHx*JVVB-`Bh_Cdz$0*H>K_@hEjkr)EH)~M nT_xa0rZ2p8v=`VMwgSqw1s!`SXB7Rn?en9IvUHWCN$~#w$6JiI literal 0 HcmV?d00001 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/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..38fa3a36e5 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; 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' From 8bd2b5b3d794ac36972731ccac650b0a9d4d6961 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 5 Jun 2019 12:14:14 +0100 Subject: [PATCH 097/807] Fix detection of current window index in CastPlayer Issue:#5955 PiperOrigin-RevId: 251616118 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/cast/CastPlayer.java | 23 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e03a0d2dc9..5128abba46 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -55,6 +55,8 @@ * Fix bug caused by parallel adaptive track selection using `Format`s without bitrate information ([#5971](https://github.com/google/ExoPlayer/issues/5971)). +* Fix bug in `CastPlayer.getCurrentWindowIndex()` + ([#5955](https://github.com/google/ExoPlayer/issues/5955)). ### 2.10.1 ### 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 8f15fb8789..db6f71286e 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 @@ -552,7 +552,17 @@ public final class CastPlayer extends BasePlayer { notificationsBatch.add( new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } - int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); + 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( @@ -565,7 +575,6 @@ public final class CastPlayer extends BasePlayer { new ListenerNotificationTask( listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } - maybeUpdateTimelineAndNotify(); flushNotifications(); } @@ -715,16 +724,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) { From 3490bea339e4b7c7177a70bf878050506acad794 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 5 Jun 2019 12:28:37 +0100 Subject: [PATCH 098/807] Simplify re-creation of the CastPlayer queue in the Cast demo app PiperOrigin-RevId: 251617354 --- .../exoplayer2/castdemo/DefaultReceiverPlayerManager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index a837bd77e5..bc38cbdb8a 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -69,7 +69,6 @@ import org.json.JSONObject; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; - private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -271,9 +270,6 @@ import org.json.JSONObject; public void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { updateCurrentItemIndex(); - if (currentPlayer == castPlayer && timeline.isEmpty()) { - castMediaQueueCreationPending = true; - } } // CastPlayer.SessionAvailabilityListener implementation. @@ -335,7 +331,6 @@ import org.json.JSONObject; this.currentPlayer = currentPlayer; // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { exoPlayer.prepare(concatenatingMediaSource); } @@ -355,12 +350,11 @@ import org.json.JSONObject; */ private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { maybeSetCurrentItemAndNotify(itemIndex); - if (castMediaQueueCreationPending) { + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; for (int i = 0; i < items.length; i++) { items[i] = buildMediaQueueItem(mediaQueue.get(i)); } - castMediaQueueCreationPending = false; castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); } else { currentPlayer.seekTo(itemIndex, positionMs); From cfa837df5c20a217de574f6d8d53ef161716f973 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 00:42:40 +0100 Subject: [PATCH 099/807] Don't throw DecoderQueryException from getCodecMaxSize It's only thrown in an edge case on API level 20 and below. If it is thrown it causes playback failure when playback could succeed, by throwing up through configureCodec. It seems better just to catch the exception and have the codec be configured using the format's own width and height. PiperOrigin-RevId: 251745539 --- .../mediacodec/MediaCodecRenderer.java | 4 +-- .../video/MediaCodecVideoRenderer.java | 35 ++++++++++--------- .../testutil/DebugRenderersFactory.java | 4 +-- 3 files changed, 20 insertions(+), 23 deletions(-) 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 d636467303..d00b218c38 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 @@ -455,15 +455,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) { 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 33eb1095c3..45ab06db45 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 @@ -581,8 +581,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException { + float codecOperatingRate) { codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( @@ -1210,11 +1209,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); @@ -1264,17 +1261,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * 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; @@ -1292,12 +1287,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; } } } 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 e1243d34ba..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; @@ -115,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 From 624bb6b8d1709852e8cffc5c67eb7d5debd9e848 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 01:00:22 +0100 Subject: [PATCH 100/807] Attach timestamp to ExoPlaybackException PiperOrigin-RevId: 251748542 --- .../com/google/android/exoplayer2/ExoPlaybackException.java | 6 ++++++ 1 file changed, 6 insertions(+) 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(); } /** From 5b02f92dad9c5725ed32e67f326b1499ca3e5dde Mon Sep 17 00:00:00 2001 From: sr1990 Date: Mon, 10 Jun 2019 22:15:04 -0700 Subject: [PATCH 101/807] [Patch V2] Support signalling of last segment number via supplemental descriptor in mpd --- .../source/dash/DefaultDashChunkSource.java | 28 +------ .../dash/manifest/DashManifestParser.java | 35 ++++++--- .../source/dash/manifest/SegmentBase.java | 74 ++++++++++++++++++- 3 files changed, 100 insertions(+), 37 deletions(-) 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 2877b2a1cc..02b2990193 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 @@ -42,7 +42,6 @@ import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.Descriptor; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -326,35 +325,10 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - List listDescriptors; - Integer lastSegmentNumberSchemeIdUri = Integer.MAX_VALUE; - String sampleMimeType = trackSelection.getFormat(periodIndex).sampleMimeType; - - if (sampleMimeType.contains("video") || sampleMimeType.contains("audio")) { - - int track_type = sampleMimeType.contains("video")? C.TRACK_TYPE_VIDEO : C.TRACK_TYPE_AUDIO; - - if (!manifest.getPeriod(periodIndex).adaptationSets.get(manifest.getPeriod(periodIndex) - .getAdaptationSetIndex(track_type)).supplementalProperties.isEmpty()) { - listDescriptors = manifest.getPeriod(periodIndex).adaptationSets - .get(manifest.getPeriod(periodIndex).getAdaptationSetIndex(track_type)) - .supplementalProperties; - for ( Descriptor descriptor: listDescriptors ) { - if (descriptor.schemeIdUri.equalsIgnoreCase - ("http://dashif.org/guidelines/last-segment-number")) { - lastSegmentNumberSchemeIdUri = Integer.valueOf(descriptor.value); - } - } - } - } - long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); long lastAvailableSegmentNum = - Math.min(representationHolder. - getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs), - lastSegmentNumberSchemeIdUri); - + representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); long segmentNum = 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 64ec1adb43..37230696f8 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 @@ -242,7 +242,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); + segmentBase = parseSegmentTemplate(xpp, null,null); } else { maybeSkipTag(xpp); } @@ -323,7 +323,8 @@ public class DashManifestParser extends DefaultHandler language, roleDescriptors, accessibilityDescriptors, - segmentBase); + segmentBase, + supplementalProperties); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -332,7 +333,7 @@ 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); + segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase,supplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { @@ -485,7 +486,8 @@ public class DashManifestParser extends DefaultHandler String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, - SegmentBase segmentBase) + SegmentBase segmentBase, + ArrayList parentSupplementalProperties) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -517,7 +519,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); + segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, + parentSupplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -756,7 +759,8 @@ public class DashManifestParser extends DefaultHandler startNumber, duration, timeline, segments); } - protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent) + protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent, + ArrayList parentSupplementalProperties) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -788,7 +792,8 @@ public class DashManifestParser extends DefaultHandler } return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + startNumber, duration, timeline, initializationTemplate, mediaTemplate, + parentSupplementalProperties); } protected SegmentTemplate buildSegmentTemplate( @@ -799,9 +804,21 @@ public class DashManifestParser extends DefaultHandler long duration, List timeline, UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + UrlTemplate mediaTemplate,ArrayList supplementalProperties ) { + + if (supplementalProperties != null) { + for (Descriptor descriptor : supplementalProperties) { + if (descriptor.schemeIdUri.equalsIgnoreCase + ("http://dashif.org/guidelines/last-segment-number")) { + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, + startNumber, Integer.valueOf(descriptor.value),duration, timeline, + initializationTemplate, mediaTemplate); + } + } + } + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + startNumber,duration, timeline, initializationTemplate, mediaTemplate); } /** 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 f033232590..720c20eed2 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 @@ -102,6 +102,7 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; /* package */ final List segmentTimeline; + /* package */ final int endNumber; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -128,6 +129,38 @@ public abstract class SegmentBase { this.startNumber = startNumber; this.duration = duration; this.segmentTimeline = segmentTimeline; + this.endNumber = C.INDEX_UNSET; + } + + /** + * @param initialization A {@link RangedUri} corresponding to initialization data, if such data + * exists. + * @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 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. + */ + public MultiSegmentBase( + RangedUri initialization, + long timescale, + long presentationTimeOffset, + long startNumber, + int endNumber, + long duration, + List segmentTimeline) { + super(initialization, timescale, presentationTimeOffset); + this.startNumber = startNumber; + this.duration = duration; + this.segmentTimeline = segmentTimeline; + this.endNumber = endNumber; } /** @see DashSegmentIndex#getSegmentNum(long, long) */ @@ -312,6 +345,43 @@ public abstract class SegmentBase { this.mediaTemplate = mediaTemplate; } + /** + * @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 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, + int endNumber, + long duration, + List segmentTimeline, + UrlTemplate initializationTemplate, + UrlTemplate mediaTemplate) { + super(initialization, timescale, presentationTimeOffset, startNumber,endNumber, + duration, segmentTimeline); + this.initializationTemplate = initializationTemplate; + this.mediaTemplate = mediaTemplate; + } + @Override public RangedUri getInitialization(Representation representation) { if (initializationTemplate != null) { @@ -338,7 +408,9 @@ public abstract class SegmentBase { @Override public int getSegmentCount(long periodDurationUs) { - if (segmentTimeline != null) { + if( endNumber != C.INDEX_UNSET) { + return endNumber; + } else if (segmentTimeline != null) { return segmentTimeline.size(); } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; From 28ee05f657c2051f6128fb0e35c859431570a5fd Mon Sep 17 00:00:00 2001 From: arodriguez Date: Fri, 14 Jun 2019 08:24:31 +0200 Subject: [PATCH 102/807] Support for UDP data source --- .../exoplayer2/upstream/DefaultDataSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 bfc9a37844..aeaa977b12 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 @@ -55,6 +55,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; @@ -66,6 +67,7 @@ public final class DefaultDataSource implements DataSource { @Nullable private DataSource assetDataSource; @Nullable private DataSource contentDataSource; @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource udpDataSource; @Nullable private DataSource dataSchemeDataSource; @Nullable private DataSource rawResourceDataSource; @@ -139,6 +141,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); } @@ -161,6 +164,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)) { @@ -199,6 +204,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(); From 1fb105bbb2bef0f543eb5e2bd909dddef623d420 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 01:00:22 +0100 Subject: [PATCH 103/807] Attach timestamp to ExoPlaybackException PiperOrigin-RevId: 251748542 --- .../com/google/android/exoplayer2/ExoPlaybackException.java | 6 ++++++ 1 file changed, 6 insertions(+) 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(); } /** From e525c1c59ea0415d9c8c912c613cf363272e89fe Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Jun 2019 21:31:11 +0100 Subject: [PATCH 104/807] Add CronetDataSource.read(ByteBuffer) method that writes directly into caller's buffer. PiperOrigin-RevId: 251915459 --- .../ext/cronet/CronetDataSource.java | 189 ++++++++-- .../ext/cronet/CronetDataSourceTest.java | 328 ++++++++++++++++++ 2 files changed, 492 insertions(+), 25 deletions(-) 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 a1ee80767d..7e30d924a0 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 @@ -40,6 +40,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -504,32 +505,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // Fill readBuffer with more data from Cronet. operation.close(); readBuffer.clear(); - castNonNull(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. - this.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 readBuffer to avoid it being written to by this - // operation during a subsequent request. - this.readBuffer = null; - throw new HttpDataSourceException( - e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); - } + readInternal(castNonNull(readBuffer)); - if (exception != null) { - throw new HttpDataSourceException( - exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); - } else if (finished) { + if (finished) { bytesRemaining = 0; return C.RESULT_END_OF_INPUT; } else { @@ -554,6 +532,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 {@codes 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) { @@ -655,6 +742,47 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; } + /** + * 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. + */ + 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 (Objects.equals(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 (Objects.equals(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")) { @@ -738,6 +866,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { 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 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 a01c5e84b6..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 @@ -554,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(); @@ -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; + } } From 8a2871ed51d336a84c1dd2ea2306f33895aa2c3e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Jun 2019 01:26:45 +0100 Subject: [PATCH 105/807] Allow protected access to surface in MediaCodecVideoRenderer PiperOrigin-RevId: 251961318 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 ++++ 1 file changed, 4 insertions(+) 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 45ab06db45..f60dbf3cb7 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 @@ -1603,6 +1603,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return deviceNeedsSetOutputSurfaceWorkaround; } + protected Surface getSurface() { + return surface; + } + protected static final class CodecMaxValues { public final int width; From 3bff79f56f5c2f80f225f626b86166034300017d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 7 Jun 2019 16:35:56 +0100 Subject: [PATCH 106/807] Wrap MediaCodec exceptions in DecoderException and report as renderer error. We currently report MediaCodec exceptions as unexpected exceptions instead of as renderer error. All such exceptions are now wrapped in a new DecoderException to allow adding more details to the exception. PiperOrigin-RevId: 252054486 --- RELEASENOTES.md | 2 + .../exoplayer2/demo/PlayerActivity.java | 4 +- .../mediacodec/MediaCodecRenderer.java | 130 +++++++++++++----- .../video/MediaCodecVideoRenderer.java | 23 ++++ 4 files changed, 119 insertions(+), 40 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5128abba46..bc9f64a001 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,8 @@ * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Add VR player demo. +* Wrap decoder exceptions in a new `DecoderException` class and report as + renderer error. ### 2.10.2 ### 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 82fb8bb9f5..929b579b4c 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 @@ -680,7 +680,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) { @@ -695,7 +695,7 @@ public class PlayerActivity extends AppCompatActivity errorString = getString( R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); + decoderInitializationException.codecInfo.name); } } } 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 d00b218c38..4b7bab2cfa 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 @@ -80,14 +80,13 @@ 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 @@ -103,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); } @@ -125,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; } @@ -144,7 +146,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { getCause(), mimeType, secureDecoderRequired, - decoderName, + codecInfo, diagnosticInfo, fallbackException); } @@ -159,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. */ @@ -637,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(); } /** @@ -725,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(); @@ -785,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 { @@ -1701,6 +1742,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return cryptoInfo; } + private static boolean isMediaCodecException(IllegalStateException error) { + if (Util.SDK_INT >= 21) { + return isMediaCodecExceptionV21(error); + } + 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. 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 f60dbf3cb7..c864adfa68 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; @@ -1260,6 +1277,12 @@ 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 From cc337a3e2d42118acfa32bd33eff0b7d207605ca Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Jun 2019 23:09:13 +0100 Subject: [PATCH 107/807] Update nullness annotations. PiperOrigin-RevId: 252127811 --- .../exoplayer2/source/chunk/BaseMediaChunk.java | 3 ++- .../android/exoplayer2/source/chunk/ChunkHolder.java | 8 ++++---- .../exoplayer2/source/chunk/ChunkSampleStream.java | 12 ++++++------ .../android/exoplayer2/source/chunk/MediaChunk.java | 3 ++- 4 files changed, 14 insertions(+), 12 deletions(-) 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/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 d9b28d9c92..499aea6a0c 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 @@ -60,8 +60,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; @@ -104,8 +104,8 @@ public class ChunkSampleStream implements SampleStream, S @Deprecated public ChunkSampleStream( int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback> callback, Allocator allocator, @@ -140,8 +140,8 @@ public class ChunkSampleStream implements SampleStream, S */ public ChunkSampleStream( int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback> callback, Allocator allocator, 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) { From 3fcae68432ae1cd07b8293cba0cb490f5aefdb4b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 13 Jun 2019 12:57:09 +0100 Subject: [PATCH 108/807] Add flags to DrmSessionManager PiperOrigin-RevId: 253006112 --- .../exoplayer2/drm/DrmSessionManager.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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 168783cf1c..375faff797 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,37 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; +import androidx.annotation.IntDef; 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 { + /** 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}. @@ -45,4 +69,10 @@ public interface DrmSessionManager { * @return The DRM session. */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); + + /** Returns flags that control the handling of DRM protected content. */ + @Flags + default int getFlags() { + return 0; + } } From 2ce28a1620672faced2a9e08931c40f3053b397c Mon Sep 17 00:00:00 2001 From: arodriguez Date: Fri, 14 Jun 2019 08:24:31 +0200 Subject: [PATCH 109/807] Support for UDP data source --- .../exoplayer2/upstream/DefaultDataSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 bfc9a37844..aeaa977b12 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 @@ -55,6 +55,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; @@ -66,6 +67,7 @@ public final class DefaultDataSource implements DataSource { @Nullable private DataSource assetDataSource; @Nullable private DataSource contentDataSource; @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource udpDataSource; @Nullable private DataSource dataSchemeDataSource; @Nullable private DataSource rawResourceDataSource; @@ -139,6 +141,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); } @@ -161,6 +164,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)) { @@ -199,6 +204,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(); From 04524a688ded24b108abc574360f1902e077860c Mon Sep 17 00:00:00 2001 From: Tim Balsfulland Date: Sat, 15 Jun 2019 15:59:57 +0200 Subject: [PATCH 110/807] Add convenience constructors for notification channel descriptions --- .../exoplayer2/offline/DownloadService.java | 44 ++++++++++++- .../exoplayer2/util/NotificationUtil.java | 37 +++++++++++ .../ui/PlayerNotificationManager.java | 61 +++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) 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..2110ac2c48 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,6 +174,7 @@ 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; private int lastStartId; @@ -239,16 +240,53 @@ public abstract class DownloadService extends Service { long foregroundNotificationUpdateInterval, @Nullable String channelId, @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0 + ); + } + + /** + * Creates a DownloadService. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelId An id for a low priority notification channel to create, or {@code null} if + * the app will take care of creating a notification channel if needed. If specified, must be + * 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 + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + */ + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @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 +581,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); 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..910a8efbe9 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 @@ -80,11 +80,48 @@ public final class NotificationUtil { */ public static void createNotificationChannel( Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + createNotificationChannel( + context, + id, + nameResourceId, + importance, + /* descriptionResourceId= */ 0); + } + + /** + * Creates a notification channel that notifications can be posted to. See {@link + * NotificationChannel} and {@link + * NotificationManager#createNotificationChannel(NotificationChannel)} for details. + * + * @param context A {@link Context}. + * @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. + * @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}. + * @param descriptionResourceId A String resource identifier for the user visible description of + * the channel. You can change the description of this channel when the system locale changes + * by listening for the {@link Intent#ACTION_LOCALE_CHANGED} broadcast. Ignored if set to 0. + */ + public static void createNotificationChannel( + Context context, + String id, + @StringRes int nameResourceId, + @Importance int importance, + @StringRes int descriptionResourceId) { 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/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..0c34cc7bba 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 @@ -414,6 +414,38 @@ public class PlayerNotificationManager { context, channelId, notificationId, mediaDescriptionAdapter); } + /** + * Creates a notification manager and a low-priority notification channel with the specified + * {@code channelId} and {@code channelName}. + * + *

    If the player notification manager is intended to be used within a foreground service, + * {@link #createWithNotificationChannel(Context, String, int, int, MediaDescriptionAdapter, + * NotificationListener)} should be used to which a {@link NotificationListener} can be passed. + * This way you'll receive the notification to put the service into the foreground by calling + * {@link android.app.Service#startForeground(int, Notification)}. + * + * @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 channelDescription A String resource identifier for the user visible description of the + * channel. + * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. + */ + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + @StringRes int channelDescription, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + NotificationUtil.createNotificationChannel( + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); + return new PlayerNotificationManager( + context, channelId, notificationId, mediaDescriptionAdapter); + } + /** * 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 @@ -440,6 +472,35 @@ public class PlayerNotificationManager { context, channelId, 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 + * parameter will be notified when the notification is created and cancelled. + * + * @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 channelDescription A String resource identifier for the user visible description of the + * channel. + * @param notificationId The id of the notification. + * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. + * @param notificationListener The {@link NotificationListener}. + */ + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + @StringRes int channelDescription, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, + @Nullable NotificationListener notificationListener) { + NotificationUtil.createNotificationChannel( + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); + return new PlayerNotificationManager( + context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); + } + /** * Creates a notification manager using the specified notification {@code channelId}. The caller * is responsible for creating the notification channel. From b29731d501c9ee97bdc3248f2a20e3c0e9374ea8 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Tue, 18 Jun 2019 11:27:37 +0200 Subject: [PATCH 111/807] Parse text track subtype into Format.roleflags. --- .../manifest/SsManifestParser.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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..7b7c539aee 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 @@ -586,6 +586,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 +646,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"; @@ -710,6 +712,18 @@ public class SsManifestParser implements ParsingLoadable.Parser { language); } else if (type == C.TRACK_TYPE_TEXT) { String language = (String) getNormalizedAttribute(KEY_LANGUAGE); + String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE); + int roleFlags = 0; + switch (subType) { + case "CAPT": + roleFlags |= C.ROLE_FLAG_CAPTION; + break; + case "DESC": + roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + break; + case "SUBT": + break; + } format = Format.createTextContainerFormat( id, @@ -719,7 +733,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { /* codecs= */ null, bitrate, /* selectionFlags= */ 0, - /* roleFlags= */ 0, + /* roleFlags= */ roleFlags, language); } else { format = From 1266d5967be77af9f0be5636d10eea940e983b43 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 17 Jun 2019 09:56:50 +0100 Subject: [PATCH 112/807] Fix all FIXME comments. These are mostly nullability issues. PiperOrigin-RevId: 253537068 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 +- .../java/com/google/android/exoplayer2/drm/ExoMediaDrm.java | 3 ++- .../google/android/exoplayer2/drm/FrameworkMediaDrm.java | 5 +---- .../android/exoplayer2/scheduler/PlatformScheduler.java | 6 +++--- 4 files changed, 7 insertions(+), 9 deletions(-) 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 84e984445a..4e18df04e3 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 @@ -533,7 +533,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/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 49915f3af5..6bd8d9688f 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; 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..609abd4e1e 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 Date: Mon, 17 Jun 2019 14:26:55 +0100 Subject: [PATCH 113/807] Remove Objects.equals use from CronetDataSource Objects was added in API 19. PiperOrigin-RevId: 253567490 --- .../android/exoplayer2/ext/cronet/CronetDataSource.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7e30d924a0..2cd40c8d70 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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Predicate; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -40,7 +41,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -759,7 +759,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } catch (InterruptedException e) { // The operation is ongoing so replace buffer to avoid it being written to by this // operation during a subsequent request. - if (Objects.equals(buffer, readBuffer)) { + if (Util.areEqual(buffer, readBuffer)) { readBuffer = null; } Thread.currentThread().interrupt(); @@ -770,7 +770,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } catch (SocketTimeoutException e) { // The operation is ongoing so replace buffer to avoid it being written to by this // operation during a subsequent request. - if (Objects.equals(buffer, readBuffer)) { + if (Util.areEqual(buffer, readBuffer)) { readBuffer = null; } throw new HttpDataSourceException( From f90cbcdffd066761789fa40c87147992ae819b92 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 17 Jun 2019 16:30:10 +0100 Subject: [PATCH 114/807] Add MRC continuous play API to IMA android sdk. Details in go/ima-mrc-continuous-play Corresponding js webcore changes is in . NoExternal PiperOrigin-RevId: 253585186 --- .../google/android/exoplayer2/ext/ima/FakeAdsRequest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java index 7c2c8a6e0b..3c34d9b577 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java @@ -105,6 +105,11 @@ public final class FakeAdsRequest implements AdsRequest { throw new UnsupportedOperationException(); } + @Override + public void setContinuousPlayback(boolean b) { + throw new UnsupportedOperationException(); + } + @Override public void setContentDuration(float v) { throw new UnsupportedOperationException(); From c05cb3f6f46a9c03e7de913c151e8155e1f20aeb Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 17 Jun 2019 17:13:32 +0100 Subject: [PATCH 115/807] Add bug report section to question and content_not_playing issue templates. PiperOrigin-RevId: 253593267 --- .github/ISSUE_TEMPLATE/bug.md | 9 +++++---- .github/ISSUE_TEMPLATE/content_not_playing.md | 14 +++++++++++--- .github/ISSUE_TEMPLATE/question.md | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index a4996278bd..c0980df440 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -36,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 ff29f3a7d1..c8d4668a6a 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -33,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". @@ -44,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. + + + + + + +

      +
      + +
      +
      +
      +
      +
      + + +
      +
      +
      + for debugging
      purpose only +
      +
      +
      +
        +
      • prepare
      • +
      • prev
      • +
      • rewind
      • +
      • play
      • +
      • pause
      • +
      • ffwd
      • +
      • next
      • +
      • stop
      • +
      +
      +
      +
      + + + diff --git a/cast_receiver_app/app-desktop/src/main.js b/cast_receiver_app/app-desktop/src/main.js new file mode 100644 index 0000000000..5645d70787 --- /dev/null +++ b/cast_receiver_app/app-desktop/src/main.js @@ -0,0 +1,170 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.debug'); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); +const Player = goog.require('exoplayer.cast.Player'); +const PlayerControls = goog.require('exoplayer.cast.PlayerControls'); +const ShakaPlayer = goog.require('shaka.Player'); +const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); +const installAll = goog.require('shaka.polyfill.installAll'); +const util = goog.require('exoplayer.cast.util'); + +/** @type {!Array} */ +let queue = []; +/** @type {number} */ +let uuidCounter = 1; + +// install all polyfills for the Shaka player +installAll(); + +/** + * Listens for player state changes and logs the state to the console. + * + * @param {!PlayerState} playerState The player state. + */ +const playerListener = function(playerState) { + util.log(['playerState: ', playerState.playbackPosition, playerState]); + queue = playerState.mediaQueue; + highlightCurrentItem( + playerState.playbackPosition && playerState.playbackPosition.uuid ? + playerState.playbackPosition.uuid : + ''); + if (playerState.playWhenReady && playerState.playbackState === 'READY') { + document.body.classList.add('playing'); + } else { + document.body.classList.remove('playing'); + } + if (playerState.playbackState === 'IDLE' && queue.length === 0) { + // Stop has been called or player not yet prepared. + resetSampleList(); + } +}; + +/** + * Highlights the currently playing item in the samples list. + * + * @param {string} uuid + */ +const highlightCurrentItem = function(uuid) { + const actions = /** @type {!NodeList} */ ( + document.querySelectorAll('#media-actions .action')); + for (let action of actions) { + if (action.dataset['uuid'] === uuid) { + action.classList.add('prepared'); + } else { + action.classList.remove('prepared'); + } + } +}; + +/** + * Makes sure all items reflect being removed from the timeline. + */ +const resetSampleList = function() { + const actions = /** @type {!NodeList} */ ( + document.querySelectorAll('#media-actions .action')); + for (let action of actions) { + action.classList.remove('prepared'); + delete action.dataset['uuid']; + } +}; + +/** + * If the arguments provide a valid media item it is added to the player. + * + * @param {!MediaItem} item The media item. + * @return {string} The uuid which has been created for the item before adding. + */ +const addQueueItem = function(item) { + if (!(item.media && item.media.uri && item.mimeType)) { + throw Error('insufficient arguments to add a queue item'); + } + item.uuid = 'uuid-' + uuidCounter++; + player.addQueueItems(queue.length, [item], /* playbackOrder= */ undefined); + return item.uuid; +}; + +/** + * An event listener which listens for actions. + * + * @param {!Event} ev The DOM event. + */ +const handleAction = (ev) => { + let target = ev.target; + while (target !== document.body && !target.dataset['action']) { + target = target.parentNode; + } + if (!target || !target.dataset['action']) { + return; + } + switch (target.dataset['action']) { + case 'player.addItems': + if (target.dataset['uuid']) { + player.removeQueueItems([target.dataset['uuid']]); + delete target.dataset['uuid']; + } else { + const uuid = addQueueItem(/** @type {!MediaItem} */ + (JSON.parse(target.dataset['item']))); + target.dataset['uuid'] = uuid; + } + break; + } +}; + +/** + * Appends samples to the list of media item actions. + * + * @param {!Array} mediaItems The samples to add. + */ +const appendSamples = function(mediaItems) { + const samplesList = document.getElementById('media-actions'); + mediaItems.forEach((item) => { + const div = /** @type {!HTMLElement} */ (document.createElement('div')); + div.classList.add('action', 'button'); + div.dataset['action'] = 'player.addItems'; + div.dataset['item'] = JSON.stringify(item); + div.appendChild(document.createTextNode(item.title)); + const marker = document.createElement('span'); + marker.classList.add('queue-marker'); + div.appendChild(marker); + samplesList.appendChild(div); + }); +}; + +/** @type {!HTMLMediaElement} */ +const mediaElement = + /** @type {!HTMLMediaElement} */ (document.getElementById('video')); +// Workaround for https://github.com/google/shaka-player/issues/1819 +// TODO(bachinger) Remove line when better fix available. +new SimpleTextDisplayer(mediaElement); +/** @type {!ShakaPlayer} */ +const shakaPlayer = new ShakaPlayer(mediaElement); +/** @type {!Player} */ +const player = new Player(shakaPlayer, new ConfigurationFactory()); +new PlayerControls(player, 'exo_controls'); +new PlaybackInfoView(player, 'exo_playback_info'); + +// register listeners +document.body.addEventListener('click', handleAction); +player.addPlayerListener(playerListener); + +// expose the player for debugging purposes. +window['player'] = player; + +exports.appendSamples = appendSamples; diff --git a/cast_receiver_app/app-desktop/src/player_controls.js b/cast_receiver_app/app-desktop/src/player_controls.js new file mode 100644 index 0000000000..e29f74148c --- /dev/null +++ b/cast_receiver_app/app-desktop/src/player_controls.js @@ -0,0 +1,164 @@ +/* + * 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. + */ +goog.module('exoplayer.cast.PlayerControls'); + +const Player = goog.require('exoplayer.cast.Player'); + +/** + * A simple UI to control the player. + * + */ +class PlayerControls { + /** + * @param {!Player} player The player. + * @param {string} containerId The id of the container element. + */ + constructor(player, containerId) { + /** @const @private {!Player} */ + this.player_ = player; + /** @const @private {?Element} */ + this.root_ = document.getElementById(containerId); + /** @const @private {?Element} */ + this.playButton_ = this.root_.querySelector('#button_play'); + /** @const @private {?Element} */ + this.pauseButton_ = this.root_.querySelector('#button_pause'); + /** @const @private {?Element} */ + this.previousButton_ = this.root_.querySelector('#button_previous'); + /** @const @private {?Element} */ + this.nextButton_ = this.root_.querySelector('#button_next'); + + const previous = () => { + const index = player.getPreviousWindowIndex(); + if (index !== -1) { + player.seekToWindow(index, 0); + } + }; + const next = () => { + const index = player.getNextWindowIndex(); + if (index !== -1) { + player.seekToWindow(index, 0); + } + }; + const rewind = () => { + player.seekToWindow( + player.getCurrentWindowIndex(), + player.getCurrentPositionMs() - 15000); + }; + const fastForward = () => { + player.seekToWindow( + player.getCurrentWindowIndex(), + player.getCurrentPositionMs() + 30000); + }; + const actions = { + 'pwr_1': (ev) => player.setPlayWhenReady(true), + 'pwr_0': (ev) => player.setPlayWhenReady(false), + 'rewind': rewind, + 'fastforward': fastForward, + 'previous': previous, + 'next': next, + 'prepare': (ev) => player.prepare(), + 'stop': (ev) => player.stop(true), + 'remove_queue_item': (ev) => { + player.removeQueueItems([ev.target.dataset.id]); + }, + }; + /** + * @param {!Event} ev The key event. + * @return {boolean} true if the key event has been handled. + */ + const keyListener = (ev) => { + const key = /** @type {!KeyboardEvent} */ (ev).key; + switch (key) { + case 'ArrowUp': + case 'k': + previous(); + ev.preventDefault(); + return true; + case 'ArrowDown': + case 'j': + next(); + ev.preventDefault(); + return true; + case 'ArrowLeft': + case 'h': + rewind(); + ev.preventDefault(); + return true; + case 'ArrowRight': + case 'l': + fastForward(); + ev.preventDefault(); + return true; + case ' ': + case 'p': + player.setPlayWhenReady(!player.getPlayWhenReady()); + ev.preventDefault(); + return true; + } + return false; + }; + document.addEventListener('keydown', keyListener); + this.root_.addEventListener('click', function(ev) { + const method = ev.target['dataset']['method']; + if (actions[method]) { + actions[method](ev); + } + return true; + }); + player.addPlayerListener((playerState) => this.updateUi(playerState)); + player.invalidate(); + this.setVisible_(true); + } + + /** + * Syncs the ui with the player state. + * + * @param {!PlayerState} playerState The state of the player to be reflected + * by the UI. + */ + updateUi(playerState) { + if (playerState.playWhenReady) { + this.playButton_.style.display = 'none'; + this.pauseButton_.style.display = 'inline-block'; + } else { + this.playButton_.style.display = 'inline-block'; + this.pauseButton_.style.display = 'none'; + } + if (this.player_.getNextWindowIndex() === -1) { + this.nextButton_.style.visibility = 'hidden'; + } else { + this.nextButton_.style.visibility = 'visible'; + } + if (this.player_.getPreviousWindowIndex() === -1) { + this.previousButton_.style.visibility = 'hidden'; + } else { + this.previousButton_.style.visibility = 'visible'; + } + } + + /** + * @private + * @param {boolean} visible If `true` thie controls are shown. If `false` the + * controls are hidden. + */ + setVisible_(visible) { + if (this.root_) { + this.root_.style.display = visible ? 'block' : 'none'; + } + } +} + +exports = PlayerControls; diff --git a/cast_receiver_app/app-desktop/src/samples.js b/cast_receiver_app/app-desktop/src/samples.js new file mode 100644 index 0000000000..2d190bdef4 --- /dev/null +++ b/cast_receiver_app/app-desktop/src/samples.js @@ -0,0 +1,70 @@ +/* + * 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. + */ +goog.module('exoplayer.cast.samples'); + +const {appendSamples} = goog.require('exoplayer.cast.debug'); + +appendSamples([ + { + title: 'DASH: multi-period', + mimeType: 'application/dash+xml', + media: { + uri: 'https://storage.googleapis.com/exoplayer-test-media-internal-6383' + + '4241aced7884c2544af1a3452e01/dash/multi-period/two-periods-minimal' + + '-duration.mpd', + }, + }, + { + title: 'HLS: Angel one', + mimeType: 'application/vnd.apple.mpegurl', + media: { + uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hl' + + 's.m3u8', + }, + }, + { + title: 'MP4: Elephants dream', + mimeType: 'video/*', + media: { + uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/' + + 'ElephantsDream.mp4', + }, + }, + { + title: 'MKV: Android screens', + mimeType: 'video/*', + media: { + uri: 'https://storage.googleapis.com/exoplayer-test-media-1/mkv/android' + + '-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv', + }, + }, + { + title: 'WV: HDCP not specified', + mimeType: 'application/dash+xml', + media: { + uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd', + }, + drmSchemes: [ + { + licenseServer: { + uri: 'https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1' + + 'c&provider=widevine_test', + }, + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + }, + ], + }, +]); diff --git a/cast_receiver_app/app-desktop/src/samples_internal.js b/cast_receiver_app/app-desktop/src/samples_internal.js new file mode 100644 index 0000000000..71b05eb2c1 --- /dev/null +++ b/cast_receiver_app/app-desktop/src/samples_internal.js @@ -0,0 +1,79 @@ +/* + * 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. + */ +goog.module('exoplayer.cast.samplesinternal'); + +const {appendSamples} = goog.require('exoplayer.cast.debug'); + +appendSamples([ + { + title: 'DAS: VOD', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dash-pvr.zahs.tv/hd/manifest.mpd', + }, + }, + { + title: 'MP3', + mimeType: 'audio/*', + media: { + uri: 'http://www.noiseaddicts.com/samples_1w72b820/4190.mp3', + }, + }, + { + title: 'DASH: live', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dash-live.zahs.tv/sd/manifest.mpd', + }, + }, + { + title: 'HLS: live', + mimeType: 'application/vnd.apple.mpegurl', + media: { + uri: 'https://demo-hls5-live.zahs.tv/sd/master.m3u8', + }, + }, + { + title: 'Live DASH (HD/Widevine)', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine.mpd', + }, + drmSchemes: [ + { + licenseServer: { + uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', + }, + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + }, + ], + }, + { + title: 'VOD DASH (HD/Widevine)', + mimeType: 'application/dash+xml', + media: { + uri: 'https://demo-dashenc-pvr.zahs.tv/hd/widevine.mpd', + }, + drmSchemes: [ + { + licenseServer: { + uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', + }, + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + }, + ], + }, +]); diff --git a/cast_receiver_app/app/html/index.css b/cast_receiver_app/app/html/index.css new file mode 100644 index 0000000000..dfc9b4e0e5 --- /dev/null +++ b/cast_receiver_app/app/html/index.css @@ -0,0 +1,39 @@ +/* + * 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. + */ +section, video, div, span, body, html { + border: 0; + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + background-color: #000; + height: 100%; + overflow: hidden; +} + +#exo_player_view { + background-color: #000; + height: 100%; + position: relative; +} + +#exo_video { + height: 100%; + width: 100%; +} + diff --git a/cast_receiver_app/app/html/index.html b/cast_receiver_app/app/html/index.html new file mode 100644 index 0000000000..64de3e8a8e --- /dev/null +++ b/cast_receiver_app/app/html/index.html @@ -0,0 +1,40 @@ + + + + + + + + + +
      + +
      +
      +
      +
      +
      + + +
      +
      +
      + + + diff --git a/cast_receiver_app/app/html/playback_info_view.css b/cast_receiver_app/app/html/playback_info_view.css new file mode 100644 index 0000000000..f70695d873 --- /dev/null +++ b/cast_receiver_app/app/html/playback_info_view.css @@ -0,0 +1,59 @@ +/* + * 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. + */ + +.exo_text_label { + color: #fff; + font-family: Roboto, Arial, sans-serif; + font-size: 1em; + margin-top: 4px; +} + +#exo_playback_info { + bottom: 5%; + display: none; + left: 4%; + position: absolute; + right: 4%; + width: 92%; +} + +#exo_time_bar { + width: 100%; +} + +#exo_duration { + background-color: rgba(255, 255, 255, 0.4); + height: 0.5em; + overflow: hidden; + position: relative; + width: 100%; +} + +#exo_elapsed_time { + background-color: rgb(73, 128, 218); + height: 100%; + opacity: 1; + width: 0; +} + +#exo_duration_label { + float: right; +} + +#exo_elapsed_time_label { + float: left; +} + diff --git a/cast_receiver_app/app/src/main.js b/cast_receiver_app/app/src/main.js new file mode 100644 index 0000000000..37c6fd41eb --- /dev/null +++ b/cast_receiver_app/app/src/main.js @@ -0,0 +1,55 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.app'); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); +const Player = goog.require('exoplayer.cast.Player'); +const Receiver = goog.require('exoplayer.cast.Receiver'); +const ShakaPlayer = goog.require('shaka.Player'); +const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); +const installAll = goog.require('shaka.polyfill.installAll'); + +/** + * The ExoPlayer namespace for messages sent and received via cast message bus. + */ +const MESSAGE_NAMESPACE_EXOPLAYER = 'urn:x-cast:com.google.exoplayer.cast'; + +// installs all polyfills for the Shaka player +installAll(); +/** @type {?HTMLMediaElement} */ +const videoElement = + /** @type {?HTMLMediaElement} */ (document.getElementById('exo_video')); +if (videoElement !== null) { + // Workaround for https://github.com/google/shaka-player/issues/1819 + // TODO(bachinger) Remove line when better fix available. + new SimpleTextDisplayer(videoElement); + /** @type {!cast.framework.CastReceiverContext} */ + const castReceiverContext = cast.framework.CastReceiverContext.getInstance(); + const shakaPlayer = new ShakaPlayer(/** @type {!HTMLMediaElement} */ + (videoElement)); + const player = new Player(shakaPlayer, new ConfigurationFactory()); + new PlaybackInfoView(player, 'exo_playback_info'); + if (castReceiverContext !== null) { + const messageDispatcher = + new MessageDispatcher(MESSAGE_NAMESPACE_EXOPLAYER, castReceiverContext); + new Receiver(player, castReceiverContext, messageDispatcher); + } + // expose player for debugging purposes. + window['player'] = player; +} diff --git a/cast_receiver_app/app/src/message_dispatcher.js b/cast_receiver_app/app/src/message_dispatcher.js new file mode 100644 index 0000000000..151ac87fbe --- /dev/null +++ b/cast_receiver_app/app/src/message_dispatcher.js @@ -0,0 +1,234 @@ +/* + * 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. + */ +goog.module('exoplayer.cast.MessageDispatcher'); + +const validation = goog.require('exoplayer.cast.validation'); + +/** + * A callback function which is called by an action handler to indicate when + * processing has completed. + * + * @typedef {function(?PlayerState): undefined} + */ +const Callback = undefined; + +/** + * Handles an action sent by a sender app. + * + * @typedef {function(!Object, number, string, !Callback): undefined} + */ +const ActionHandler = undefined; + +/** + * Dispatches messages of a cast message bus to registered action handlers. + * + *

      The dispatcher listens to events of a CastMessageBus for the namespace + * passed to the constructor. The data property of the event is + * parsed as a json document and delegated to a handler registered for the given + * method. + */ +class MessageDispatcher { + /** + * @param {string} namespace The message namespace. + * @param {!cast.framework.CastReceiverContext} castReceiverContext The cast + * receiver manager. + */ + constructor(namespace, castReceiverContext) { + /** @private @const {string} */ + this.namespace_ = namespace; + /** @private @const {!cast.framework.CastReceiverContext} */ + this.castReceiverContext_ = castReceiverContext; + /** @private @const {!Array} */ + this.messageQueue_ = []; + /** @private @const {!Object} */ + this.actions_ = {}; + /** @private @const {!Object} */ + this.senderSequences_ = {}; + /** @private @const {function(string, *)} */ + this.jsonStringifyReplacer_ = (key, value) => { + if (value === Infinity || value === null) { + return undefined; + } + return value; + }; + this.castReceiverContext_.addCustomMessageListener( + this.namespace_, this.onMessage.bind(this)); + } + + /** + * Registers a handler of a given action. + * + * @param {string} method The method name for which to register the handler. + * @param {!Array>} argDefs The name and type of each argument + * or an empty array if the method has no arguments. + * @param {!ActionHandler} handler A function to process the action. + */ + registerActionHandler(method, argDefs, handler) { + this.actions_[method] = { + method, + argDefs, + handler, + }; + } + + /** + * Unregisters the handler of the given action. + * + * @param {string} action The action to unregister. + */ + unregisterActionHandler(action) { + delete this.actions_[action]; + } + + /** + * Callback to receive messages sent by sender apps. + * + * @param {!cast.framework.system.Event} event The event received from the + * sender app. + */ + onMessage(event) { + console.log('message arrived from sender', this.namespace_, event); + const message = /** @type {!ExoCastMessage} */ (event.data); + const action = this.actions_[message.method]; + if (action) { + const args = message.args; + for (let i = 0; i < action.argDefs.length; i++) { + if (!validation.validateProperty( + args, action.argDefs[i][0], action.argDefs[i][1])) { + console.warn('invalid method call', message); + return; + } + } + this.messageQueue_.push({ + senderId: event.senderId, + message: message, + handler: action.handler + }); + if (this.messageQueue_.length === 1) { + this.executeNext(); + } else { + // Do nothing. An action is executing asynchronously and will call + // executeNext when finished. + } + } else { + console.warn('handler of method not found', message); + } + } + + /** + * Executes the next message in the queue. + */ + executeNext() { + if (this.messageQueue_.length === 0) { + return; + } + const head = this.messageQueue_[0]; + const message = head.message; + const senderSequence = message.sequenceNumber; + this.senderSequences_[head.senderId] = senderSequence; + try { + head.handler(message.args, senderSequence, head.senderId, (response) => { + if (response) { + this.send(head.senderId, response); + } + this.shiftPendingMessage_(head); + }); + } catch (e) { + this.shiftPendingMessage_(head); + console.error('error while executing method : ' + message.method, e); + } + } + + /** + * Broadcasts the sender state to all sender apps registered for the + * given message namespace. + * + * @param {!PlayerState} playerState The player state to be sent. + */ + broadcast(playerState) { + this.castReceiverContext_.getSenders().forEach((sender) => { + this.send(sender.id, playerState); + }); + delete playerState.sequenceNumber; + } + + /** + * Sends the PlayerState to the given sender. + * + * @param {string} senderId The id of the sender. + * @param {!PlayerState} playerState The message to send. + */ + send(senderId, playerState) { + playerState.sequenceNumber = this.senderSequences_[senderId] || -1; + this.castReceiverContext_.sendCustomMessage( + this.namespace_, senderId, + // TODO(bachinger) Find a better solution. + JSON.parse(JSON.stringify(playerState, this.jsonStringifyReplacer_))); + } + + /** + * Notifies the message dispatcher that a given sender has disconnected from + * the receiver. + * + * @param {string} senderId The id of the sender. + */ + notifySenderDisconnected(senderId) { + delete this.senderSequences_[senderId]; + } + + /** + * Shifts the pending message and executes the next if any. + * + * @private + * @param {!Message} pendingMessage The pending message. + */ + shiftPendingMessage_(pendingMessage) { + if (pendingMessage === this.messageQueue_[0]) { + this.messageQueue_.shift(); + this.executeNext(); + } + } +} + +/** + * An item in the message queue. + * + * @record + */ +function Message() {} + +/** + * The sender id. + * + * @type {string} + */ +Message.prototype.senderId; + +/** + * The ExoCastMessage sent by the sender app. + * + * @type {!ExoCastMessage} + */ +Message.prototype.message; + +/** + * The handler function handling the message. + * + * @type {!ActionHandler} + */ +Message.prototype.handler; + +exports = MessageDispatcher; diff --git a/cast_receiver_app/app/src/receiver.js b/cast_receiver_app/app/src/receiver.js new file mode 100644 index 0000000000..5e67219e75 --- /dev/null +++ b/cast_receiver_app/app/src/receiver.js @@ -0,0 +1,191 @@ +/** + * 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. + */ + +goog.module('exoplayer.cast.Receiver'); + +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const Player = goog.require('exoplayer.cast.Player'); +const validation = goog.require('exoplayer.cast.validation'); + +/** + * The Receiver receives messages from a message bus and delegates to + * the player. + * + * @constructor + * @param {!Player} player The player. + * @param {!cast.framework.CastReceiverContext} context The cast receiver + * context. + * @param {!MessageDispatcher} messageDispatcher The message dispatcher to use. + */ +const Receiver = function(player, context, messageDispatcher) { + addPlayerActions(messageDispatcher, player); + addQueueActions(messageDispatcher, player); + player.addPlayerListener((playerState) => { + messageDispatcher.broadcast(playerState); + }); + + context.addEventListener( + cast.framework.system.EventType.SENDER_CONNECTED, (event) => { + messageDispatcher.send(event.senderId, player.getPlayerState()); + }); + + context.addEventListener( + cast.framework.system.EventType.SENDER_DISCONNECTED, (event) => { + messageDispatcher.notifySenderDisconnected(event.senderId); + if (event.reason === + cast.framework.system.DisconnectReason.REQUESTED_BY_SENDER && + context.getSenders().length === 0) { + window.close(); + } + }); + + // Start the cast receiver context. + context.start(); +}; + +/** + * Registers action handlers for playback messages sent by the sender app. + * + * @param {!MessageDispatcher} messageDispatcher The dispatcher. + * @param {!Player} player The player. + */ +const addPlayerActions = function(messageDispatcher, player) { + messageDispatcher.registerActionHandler( + 'player.setPlayWhenReady', [['playWhenReady', 'boolean']], + (args, senderSequence, senderId, callback) => { + const playWhenReady = args['playWhenReady']; + callback( + !player.setPlayWhenReady(playWhenReady) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.seekTo', + [ + ['uuid', 'string'], + ['positionMs', '?number'], + ], + (args, senderSequence, senderId, callback) => { + callback( + !player.seekToUuid(args['uuid'], args['positionMs']) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.setRepeatMode', [['repeatMode', 'RepeatMode']], + (args, senderSequence, senderId, callback) => { + callback( + !player.setRepeatMode(args['repeatMode']) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.setShuffleModeEnabled', [['shuffleModeEnabled', 'boolean']], + (args, senderSequence, senderId, callback) => { + callback( + !player.setShuffleModeEnabled(args['shuffleModeEnabled']) ? + player.getPlayerState() : + null); + }); + messageDispatcher.registerActionHandler( + 'player.onClientConnected', [], + (args, senderSequence, senderId, callback) => { + callback(player.getPlayerState()); + }); + messageDispatcher.registerActionHandler( + 'player.stop', [['reset', 'boolean']], + (args, senderSequence, senderId, callback) => { + player.stop(args['reset']).then(() => { + callback(null); + }); + }); + messageDispatcher.registerActionHandler( + 'player.prepare', [], (args, senderSequence, senderId, callback) => { + player.prepare(); + callback(null); + }); + messageDispatcher.registerActionHandler( + 'player.setTrackSelectionParameters', + [ + ['preferredAudioLanguage', 'string'], + ['preferredTextLanguage', 'string'], + ['disabledTextTrackSelectionFlags', 'Array'], + ['selectUndeterminedTextLanguage', 'boolean'], + ], + (args, senderSequence, senderId, callback) => { + const trackSelectionParameters = + /** @type {!TrackSelectionParameters} */ ({ + preferredAudioLanguage: args['preferredAudioLanguage'], + preferredTextLanguage: args['preferredTextLanguage'], + disabledTextTrackSelectionFlags: + args['disabledTextTrackSelectionFlags'], + selectUndeterminedTextLanguage: + args['selectUndeterminedTextLanguage'], + }); + callback( + !player.setTrackSelectionParameters(trackSelectionParameters) ? + player.getPlayerState() : + null); + }); +}; + +/** + * Registers action handlers for queue management messages sent by the sender + * app. + * + * @param {!MessageDispatcher} messageDispatcher The dispatcher. + * @param {!Player} player The player. + */ +const addQueueActions = + function (messageDispatcher, player) { + messageDispatcher.registerActionHandler( + 'player.addItems', + [ + ['index', '?number'], + ['items', 'Array'], + ['shuffleOrder', 'Array'], + ], + (args, senderSequence, senderId, callback) => { + const mediaItems = args['items']; + const index = args['index'] || player.getQueueSize(); + let addedItemCount; + if (validation.validateMediaItems(mediaItems)) { + addedItemCount = + player.addQueueItems(index, mediaItems, args['shuffleOrder']); + } + callback(addedItemCount === 0 ? player.getPlayerState() : null); + }); + messageDispatcher.registerActionHandler( + 'player.removeItems', [['uuids', 'Array']], + (args, senderSequence, senderId, callback) => { + const removedItemsCount = player.removeQueueItems(args['uuids']); + callback(removedItemsCount === 0 ? player.getPlayerState() : null); + }); + messageDispatcher.registerActionHandler( + 'player.moveItem', + [ + ['uuid', 'string'], + ['index', 'number'], + ['shuffleOrder', 'Array'], + ], + (args, senderSequence, senderId, callback) => { + const hasMoved = player.moveQueueItem( + args['uuid'], args['index'], args['shuffleOrder']); + callback(!hasMoved ? player.getPlayerState() : null); + }); +}; + +exports = Receiver; diff --git a/cast_receiver_app/app/src/validation.js b/cast_receiver_app/app/src/validation.js new file mode 100644 index 0000000000..23e2708f8e --- /dev/null +++ b/cast_receiver_app/app/src/validation.js @@ -0,0 +1,163 @@ +/** + * 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. + * + * @fileoverview A validator for messages received from sender apps. + */ + +goog.module('exoplayer.cast.validation'); + +const {getPlaybackType, PlaybackType, RepeatMode} = goog.require('exoplayer.cast.constants'); + +/** + * Media item fields. + * + * @enum {string} + */ +const MediaItemField = { + UUID: 'uuid', + MEDIA: 'media', + MIME_TYPE: 'mimeType', + DRM_SCHEMES: 'drmSchemes', + TITLE: 'title', + DESCRIPTION: 'description', + START_POSITION_US: 'startPositionUs', + END_POSITION_US: 'endPositionUs', +}; + +/** + * DrmScheme fields. + * + * @enum {string} + */ +const DrmSchemeField = { + UUID: 'uuid', + LICENSE_SERVER_URI: 'licenseServer', +}; + +/** + * UriBundle fields. + * + * @enum {string} + */ +const UriBundleField = { + URI: 'uri', + REQUEST_HEADERS: 'requestHeaders', +}; + +/** + * Validates an array of media items. + * + * @param {!Array} mediaItems An array of media items. + * @return {boolean} true if all media items are valid, otherwise false is + * returned. + */ +const validateMediaItems = function (mediaItems) { + for (let i = 0; i < mediaItems.length; i++) { + if (!validateMediaItem(mediaItems[i])) { + return false; + } + } + return true; +}; + +/** + * Validates a queue item sent to the receiver by a sender app. + * + * @param {!MediaItem} mediaItem The media item. + * @return {boolean} true if the media item is valid, false otherwise. + */ +const validateMediaItem = function (mediaItem) { + // validate minimal properties + if (!validateProperty(mediaItem, MediaItemField.UUID, 'string')) { + console.log('missing mandatory uuid', mediaItem.uuid); + return false; + } + if (!validateProperty(mediaItem.media, UriBundleField.URI, 'string')) { + console.log('missing mandatory', mediaItem.media ? 'uri' : 'media'); + return false; + } + const mimeType = mediaItem.mimeType; + if (!mimeType || getPlaybackType(mimeType) === PlaybackType.UNKNOWN) { + console.log('unsupported mime type:', mimeType); + return false; + } + // validate optional properties + if (goog.isArray(mediaItem.drmSchemes)) { + for (let i = 0; i < mediaItem.drmSchemes.length; i++) { + let drmScheme = mediaItem.drmSchemes[i]; + if (!validateProperty(drmScheme, DrmSchemeField.UUID, 'string') || + !validateProperty( + drmScheme.licenseServer, UriBundleField.URI, 'string')) { + console.log('invalid drm scheme', drmScheme); + return false; + } + } + } + if (!validateProperty(mediaItem, MediaItemField.START_POSITION_US, '?number') + || !validateProperty(mediaItem, MediaItemField.END_POSITION_US, '?number') + || !validateProperty(mediaItem, MediaItemField.TITLE, '?string') + || !validateProperty(mediaItem, MediaItemField.DESCRIPTION, '?string')) { + console.log('invalid type of one of startPositionUs, endPositionUs, title' + + ' or description', mediaItem); + return false; + } + return true; +}; + +/** + * Validates the existence and type of a property. + * + *

      Supported types: number, string, boolean, Array. + *

      Prefix the type with a ? to indicate that the property is optional. + * + * @param {?Object|?MediaItem|?UriBundle} obj The object to validate. + * @param {string} propertyName The name of the property. + * @param {string} type The type of the property. + * @return {boolean} True if valid, false otherwise. + */ +const validateProperty = function (obj, propertyName, type) { + if (typeof obj === 'undefined' || obj === null) { + return false; + } + const isOptional = type.startsWith('?'); + const value = obj[propertyName]; + if (isOptional && typeof value === 'undefined') { + return true; + } + type = isOptional ? type.substring(1) : type; + switch (type) { + case 'string': + return typeof value === 'string' || value instanceof String; + case 'number': + return typeof value === 'number' && isFinite(value); + case 'Array': + return typeof value !== 'undefined' && typeof value === 'object' + && value.constructor === Array; + case 'boolean': + return typeof value === 'boolean'; + case 'RepeatMode': + return value === RepeatMode.OFF || value === RepeatMode.ONE || + value === RepeatMode.ALL; + default: + console.warn('Unsupported type when validating an object property. ' + + 'Supported types are string, number, boolean and Array.', type); + return false; + } +}; + +exports.validateMediaItem = validateMediaItem; +exports.validateMediaItems = validateMediaItems; +exports.validateProperty = validateProperty; + diff --git a/cast_receiver_app/assemble.bazel.sh b/cast_receiver_app/assemble.bazel.sh new file mode 100755 index 0000000000..d2039a5152 --- /dev/null +++ b/cast_receiver_app/assemble.bazel.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# 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. + +## +# Assembles the html, css and javascript files which have been created by the +# bazel build in a destination directory. + +HTML_DIR=app/html +HTML_DEBUG_DIR=app-desktop/html +BIN=bazel-bin + +function usage { + echo "usage: `basename "$0"` -d=DESTINATION_DIR" +} + +for i in "$@" +do +case $i in + -d=*|--destination=*) + DESTINATION="${i#*=}" + shift # past argument=value + ;; + -h|--help) + usage + exit 0 + ;; + *) + # unknown option + ;; +esac +done + +if [ ! -d "$DESTINATION" ]; then + echo "destination directory '$DESTINATION' is not declared or is not a\ + directory" + usage + exit 1 +fi + +if [ ! -f "$BIN/app.js" ];then + echo "file $BIN/app.js not found. Did you build already with bazel?" + echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" + exit 1 +fi + +if [ ! -f "$BIN/app_desktop.js" ];then + echo "file $BIN/app_desktop.js not found. Did you build already with bazel?" + echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" + exit 1 +fi + +echo "assembling receiver and desktop app in $DESTINATION" +echo "-------" + +# cleaning up asset files in destination directory +FILES=( + app.js + app_desktop.js + app_styles.css + app_desktop_styles.css + index.html + player.html +) +for file in ${FILES[@]}; do + if [ -f $DESTINATION/$file ]; then + echo "deleting $file" + rm -f $DESTINATION/$file + fi +done +echo "-------" + +echo "copy html files to $DESTINATION" +cp $HTML_DIR/index.html $DESTINATION +cp $HTML_DEBUG_DIR/index.html $DESTINATION/player.html +echo "copy javascript files to $DESTINATION" +cp $BIN/app.js $BIN/app_desktop.js $DESTINATION +echo "copy css style to $DESTINATION" +cp $BIN/app_styles.css $BIN/app_desktop_styles.css $DESTINATION +echo "-------" + +echo "done." diff --git a/cast_receiver_app/externs/protocol.js b/cast_receiver_app/externs/protocol.js new file mode 100644 index 0000000000..d6544a6f37 --- /dev/null +++ b/cast_receiver_app/externs/protocol.js @@ -0,0 +1,489 @@ +/* + * 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. + */ + +/** + * @fileoverview Externs for messages sent by a sender app in JSON format. + * + * Fields defined here are prevented from being renamed by the js compiler. + * + * @externs + */ + +/** + * An uri bundle with an uri and request parameters. + * + * @record + */ +class UriBundle { + constructor() { + /** + * The URI. + * + * @type {string} + */ + this.uri; + + /** + * The request headers. + * + * @type {?Object} + */ + this.requestHeaders; + } +} + +/** + * @record + */ +class DrmScheme { + constructor() { + /** + * The DRM UUID. + * + * @type {string} + */ + this.uuid; + + /** + * The license URI. + * + * @type {?UriBundle} + */ + this.licenseServer; + } +} + +/** + * @record + */ +class MediaItem { + constructor() { + /** + * The uuid of the item. + * + * @type {string} + */ + this.uuid; + + /** + * The mime type. + * + * @type {string} + */ + this.mimeType; + + /** + * The media uri bundle. + * + * @type {!UriBundle} + */ + this.media; + + /** + * The DRM schemes. + * + * @type {!Array} + */ + this.drmSchemes; + + /** + * The position to start playback from. + * + * @type {number} + */ + this.startPositionUs; + + /** + * The position at which to end playback. + * + * @type {number} + */ + this.endPositionUs; + + /** + * The title of the media item. + * + * @type {string} + */ + this.title; + + /** + * The description of the media item. + * + * @type {string} + */ + this.description; + } +} + +/** + * Constraint parameters for track selection. + * + * @record + */ +class TrackSelectionParameters { + constructor() { + /** + * The preferred audio language. + * + * @type {string|undefined} + */ + this.preferredAudioLanguage; + + /** + * The preferred text language. + * + * @type {string|undefined} + */ + this.preferredTextLanguage; + + /** + * List of selection flags that are disabled for text track selections. + * + * @type {!Array} + */ + this.disabledTextTrackSelectionFlags; + + /** + * Whether a text track with undetermined language should be selected if no + * track with `preferredTextLanguage` is available, or if + * `preferredTextLanguage` is unset. + * + * @type {boolean} + */ + this.selectUndeterminedTextLanguage; + } +} + +/** + * The PlaybackPosition defined by the position, the uuid of the media item and + * the period id. + * + * @record + */ +class PlaybackPosition { + constructor() { + /** + * The current playback position in milliseconds. + * + * @type {number} + */ + this.positionMs; + + /** + * The uuid of the media item. + * + * @type {string} + */ + this.uuid; + + /** + * The id of the currently playing period. + * + * @type {string} + */ + this.periodId; + + /** + * The reason of a position discontinuity if any. + * + * @type {?string} + */ + this.discontinuityReason; + } +} + +/** + * The playback parameters. + * + * @record + */ +class PlaybackParameters { + constructor() { + /** + * The playback speed. + * + * @type {number} + */ + this.speed; + + /** + * The playback pitch. + * + * @type {number} + */ + this.pitch; + + /** + * Whether silence is skipped. + * + * @type {boolean} + */ + this.skipSilence; + } +} +/** + * The player state. + * + * @record + */ +class PlayerState { + constructor() { + /** + * The playback state. + * + * @type {string} + */ + this.playbackState; + + /** + * The playback parameters. + * + * @type {!PlaybackParameters} + */ + this.playbackParameters; + + /** + * Playback starts when ready if true. + * + * @type {boolean} + */ + this.playWhenReady; + + /** + * The current position within the media. + * + * @type {?PlaybackPosition} + */ + this.playbackPosition; + + /** + * The current window index. + * + * @type {number} + */ + this.windowIndex; + + /** + * The number of windows. + * + * @type {number} + */ + this.windowCount; + + /** + * The audio tracks. + * + * @type {!Array} + */ + this.audioTracks; + + /** + * The video tracks in case of adaptive media. + * + * @type {!Array>} + */ + this.videoTracks; + + /** + * The repeat mode. + * + * @type {string} + */ + this.repeatMode; + + /** + * Whether the shuffle mode is enabled. + * + * @type {boolean} + */ + this.shuffleModeEnabled; + + /** + * The playback order to use when shuffle mode is enabled. + * + * @type {!Array} + */ + this.shuffleOrder; + + /** + * The queue of media items. + * + * @type {!Array} + */ + this.mediaQueue; + + /** + * The media item info of the queue items if available. + * + * @type {!Object} + */ + this.mediaItemsInfo; + + /** + * The sequence number of the sender. + * + * @type {number} + */ + this.sequenceNumber; + + /** + * The player error. + * + * @type {?PlayerError} + */ + this.error; + } +} + +/** + * The error description. + * + * @record + */ +class PlayerError { + constructor() { + /** + * The error message. + * + * @type {string} + */ + this.message; + + /** + * The error code. + * + * @type {number} + */ + this.code; + + /** + * The error category. + * + * @type {number} + */ + this.category; + } +} + +/** + * A period. + * + * @record + */ +class Period { + constructor() { + /** + * The id of the period. Must be unique within a media item. + * + * @type {string} + */ + this.id; + + /** + * The duration of the period in microseconds. + * + * @type {number} + */ + this.durationUs; + } +} +/** + * Holds dynamic information for a MediaItem. + * + *

      Holds information related to preparation for a specific {@link MediaItem}. + * Unprepared items are associated with an {@link #EMPTY} info object until + * prepared. + * + * @record + */ +class MediaItemInfo { + constructor() { + /** + * The duration of the window in microseconds. + * + * @type {number} + */ + this.windowDurationUs; + + /** + * The default start position relative to the start of the window in + * microseconds. + * + * @type {number} + */ + this.defaultStartPositionUs; + + /** + * The periods conforming the media item. + * + * @type {!Array} + */ + this.periods; + + /** + * The position of the window in the first period in microseconds. + * + * @type {number} + */ + this.positionInFirstPeriodUs; + + /** + * Whether it is possible to seek within the window. + * + * @type {boolean} + */ + this.isSeekable; + + /** + * Whether the window may change when the timeline is updated. + * + * @type {boolean} + */ + this.isDynamic; + } +} + +/** + * The message envelope send by a sender app. + * + * @record + */ +class ExoCastMessage { + constructor() { + /** + * The clients message sequenec number. + * + * @type {number} + */ + this.sequenceNumber; + + /** + * The name of the method. + * + * @type {string} + */ + this.method; + + /** + * The arguments of the method. + * + * @type {!Object} + */ + this.args; + } +}; + diff --git a/cast_receiver_app/externs/shaka.js b/cast_receiver_app/externs/shaka.js new file mode 100644 index 0000000000..0af36d7b8c --- /dev/null +++ b/cast_receiver_app/externs/shaka.js @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/** + * @fileoverview Externs of the Shaka configuration. + * + * @externs + */ + +/** + * The drm configuration for the Shaka player. + * + * @record + */ +class DrmConfiguration { + constructor() { + /** + * A map of license servers with the UUID of the drm system as the key and the + * license uri as the value. + * + * @type {!Object} + */ + this.servers; + } +} + +/** + * The configuration of the Shaka player. + * + * @record + */ +class PlayerConfiguration { + constructor() { + /** + * The preferred audio language. + * + * @type {string} + */ + this.preferredAudioLanguage; + + /** + * The preferred text language. + * + * @type {string} + */ + this.preferredTextLanguage; + + /** + * The drm configuration. + * + * @type {?DrmConfiguration} + */ + this.drm; + } +} diff --git a/cast_receiver_app/src/configuration_factory.js b/cast_receiver_app/src/configuration_factory.js new file mode 100644 index 0000000000..819e52a755 --- /dev/null +++ b/cast_receiver_app/src/configuration_factory.js @@ -0,0 +1,90 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.ConfigurationFactory'); + +const {DRM_SYSTEMS} = goog.require('exoplayer.cast.constants'); + +const EMPTY_DRM_CONFIGURATION = + /** @type {!DrmConfiguration} */ (Object.freeze({ + servers: {}, + })); + +/** + * Creates the configuration of the Shaka player. + */ +class ConfigurationFactory { + /** + * Creates the Shaka player configuration. + * + * @param {!MediaItem} mediaItem The media item for which to create the + * configuration. + * @param {!TrackSelectionParameters} trackSelectionParameters The track + * selection parameters. + * @return {!PlayerConfiguration} The shaka player configuration. + */ + createConfiguration(mediaItem, trackSelectionParameters) { + const configuration = /** @type {!PlayerConfiguration} */ ({}); + this.mapLanguageConfiguration(trackSelectionParameters, configuration); + this.mapDrmConfiguration_(mediaItem, configuration); + return configuration; + } + + /** + * Maps the preferred audio and text language from the track selection + * parameters to the configuration. + * + * @param {!TrackSelectionParameters} trackSelectionParameters The selection + * parameters. + * @param {!PlayerConfiguration} playerConfiguration The player configuration. + */ + mapLanguageConfiguration(trackSelectionParameters, playerConfiguration) { + playerConfiguration.preferredAudioLanguage = + trackSelectionParameters.preferredAudioLanguage || ''; + playerConfiguration.preferredTextLanguage = + trackSelectionParameters.preferredTextLanguage || ''; + } + + /** + * Maps the drm configuration from the media item to the configuration. If no + * drm is specified for the given media item, null is assigned. + * + * @private + * @param {!MediaItem} mediaItem The media item. + * @param {!PlayerConfiguration} playerConfiguration The player configuration. + */ + mapDrmConfiguration_(mediaItem, playerConfiguration) { + if (!mediaItem.drmSchemes) { + playerConfiguration.drm = EMPTY_DRM_CONFIGURATION; + return; + } + const drmConfiguration = /** @type {!DrmConfiguration} */({ + servers: {}, + }); + let hasDrmServer = false; + mediaItem.drmSchemes.forEach((scheme) => { + const drmSystem = DRM_SYSTEMS[scheme.uuid]; + if (drmSystem && scheme.licenseServer && scheme.licenseServer.uri) { + hasDrmServer = true; + drmConfiguration.servers[drmSystem] = scheme.licenseServer.uri; + } + }); + playerConfiguration.drm = + hasDrmServer ? drmConfiguration : EMPTY_DRM_CONFIGURATION; + } +} + +exports = ConfigurationFactory; diff --git a/cast_receiver_app/src/constants.js b/cast_receiver_app/src/constants.js new file mode 100644 index 0000000000..e9600429f0 --- /dev/null +++ b/cast_receiver_app/src/constants.js @@ -0,0 +1,140 @@ +/** + * 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. + */ + +goog.module('exoplayer.cast.constants'); + +/** + * The underyling player. + * + * @enum {number} + */ +const PlaybackType = { + VIDEO_ELEMENT: 1, + SHAKA_PLAYER: 2, + UNKNOWN: 999, +}; + +/** + * Supported mime types and their playback mode. + * + * @type {!Object} + */ +const SUPPORTED_MIME_TYPES = Object.freeze({ + 'application/dash+xml': PlaybackType.SHAKA_PLAYER, + 'application/vnd.apple.mpegurl': PlaybackType.SHAKA_PLAYER, + 'application/vnd.ms-sstr+xml': PlaybackType.SHAKA_PLAYER, + 'application/x-mpegURL': PlaybackType.SHAKA_PLAYER, +}); + +/** + * Returns the playback type required for a given mime type, or + * PlaybackType.UNKNOWN if the mime type is not recognized. + * + * @param {string} mimeType The mime type. + * @return {!PlaybackType} The required playback type, or PlaybackType.UNKNOWN + * if the mime type is not recognized. + */ +const getPlaybackType = function(mimeType) { + if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) { + return PlaybackType.VIDEO_ELEMENT; + } else { + return SUPPORTED_MIME_TYPES[mimeType] || PlaybackType.UNKNOWN; + } +}; + +/** + * Error messages. + * + * @enum {string} + */ +const ErrorMessages = { + SHAKA_LOAD_ERROR: 'Error while loading media with Shaka.', + SHAKA_UNKNOWN_ERROR: 'Shaka error event captured.', + MEDIA_ELEMENT_UNKNOWN_ERROR: 'Media element error event captured.', + UNKNOWN_FATAL_ERROR: 'Fatal playback error. Shaka instance replaced.', + UNKNOWN_ERROR: 'Unknown error', +}; + +/** + * ExoPlayer's repeat modes. + * + * @enum {string} + */ +const RepeatMode = { + OFF: 'OFF', + ONE: 'ONE', + ALL: 'ALL', +}; + +/** + * Error categories. Error categories coming from Shaka are defined in [Shaka + * source + * code](https://shaka-player-demo.appspot.com/docs/api/shaka.util.Error.html). + * + * @enum {number} + */ +const ErrorCategory = { + MEDIA_ELEMENT: 0, + FATAL_SHAKA_ERROR: 1000, +}; + +/** + * An error object to be used if no media error is assigned to the `error` + * field of the media element when an error event is fired + * + * @type {!PlayerError} + */ +const UNKNOWN_ERROR = /** @type {!PlayerError} */ (Object.freeze({ + message: ErrorMessages.UNKNOWN_ERROR, + code: 0, + category: 0, +})); + +/** + * UUID for the Widevine DRM scheme. + * + * @type {string} + */ +const WIDEVINE_UUID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; + +/** + * UUID for the PlayReady DRM scheme. + * + * @type {string} + */ +const PLAYREADY_UUID = '9a04f079-9840-4286-ab92-e65be0885f95'; + +/** @type {!Object} */ +const drmSystems = {}; +drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha'; +drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready'; + +/** + * The uuids of the supported DRM systems. + * + * @type {!Object} + */ +const DRM_SYSTEMS = Object.freeze(drmSystems); + +exports.PlaybackType = PlaybackType; +exports.ErrorMessages = ErrorMessages; +exports.ErrorCategory = ErrorCategory; +exports.RepeatMode = RepeatMode; +exports.getPlaybackType = getPlaybackType; +exports.WIDEVINE_UUID = WIDEVINE_UUID; +exports.PLAYREADY_UUID = PLAYREADY_UUID; +exports.DRM_SYSTEMS = DRM_SYSTEMS; +exports.UNKNOWN_ERROR = UNKNOWN_ERROR; diff --git a/cast_receiver_app/src/playback_info_view.js b/cast_receiver_app/src/playback_info_view.js new file mode 100644 index 0000000000..22e2b8ded5 --- /dev/null +++ b/cast_receiver_app/src/playback_info_view.js @@ -0,0 +1,233 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.PlaybackInfoView'); + +const Player = goog.require('exoplayer.cast.Player'); +const Timeout = goog.require('exoplayer.cast.Timeout'); +const dom = goog.require('goog.dom'); + +/** The default timeout for hiding the UI in milliseconds. */ +const SHOW_TIMEOUT_MS = 5000; +/** The timeout for hiding the UI in audio only mode in milliseconds. */ +const SHOW_TIMEOUT_MS_AUDIO = 0; +/** The timeout for updating the UI while being displayed. */ +const UPDATE_TIMEOUT_MS = 1000; + +/** + * Formats a duration in milliseconds to a string in hh:mm:ss format. + * + * @param {number} durationMs The duration in milliseconds. + * @return {string} The duration formatted as hh:mm:ss. + */ +const formatTimestampMsAsString = function (durationMs) { + const hours = Math.floor(durationMs / 1000 / 60 / 60); + const minutes = Math.floor((durationMs / 1000 / 60) % 60); + const seconds = Math.floor((durationMs / 1000) % 60) % 60; + let timeString = ''; + if (hours > 0) { + timeString += hours + ':'; + } + if (minutes < 10) { + timeString += '0'; + } + timeString += minutes + ":"; + if (seconds < 10) { + timeString += '0'; + } + timeString += seconds; + return timeString; +}; + +/** + * A view to display information about the current media item and playback + * progress. + * + * @constructor + * @param {!Player} player The player of which to display the + * playback info. + * @param {string} viewId The id of the playback info view. + */ +const PlaybackInfoView = function (player, viewId) { + /** @const @private {!Player} */ + this.player_ = player; + /** @const @private {?Element} */ + this.container_ = document.getElementById(viewId); + /** @const @private {?Element} */ + this.elapsedTimeBar_ = document.getElementById('exo_elapsed_time'); + /** @const @private {?Element} */ + this.elapsedTimeLabel_ = document.getElementById('exo_elapsed_time_label'); + /** @const @private {?Element} */ + this.durationLabel_ = document.getElementById('exo_duration_label'); + /** @const @private {!Timeout} */ + this.hideTimeout_ = new Timeout(); + /** @const @private {!Timeout} */ + this.updateTimeout_ = new Timeout(); + /** @private {boolean} */ + this.wasPlaying_ = player.getPlayWhenReady() + && player.getPlaybackState() === Player.PlaybackState.READY; + /** @private {number} */ + this.showTimeoutMs_ = SHOW_TIMEOUT_MS; + /** @private {number} */ + this.showTimeoutMsVideo_ = this.showTimeoutMs_; + + if (this.wasPlaying_) { + this.hideAfterTimeout(); + } else { + this.show(); + } + + player.addPlayerListener((playerState) => { + if (this.container_ === null) { + return; + } + const playbackPosition = playerState.playbackPosition; + const discontinuityReason = + playbackPosition ? playbackPosition.discontinuityReason : null; + if (discontinuityReason) { + const currentMediaItem = player.getCurrentMediaItem(); + this.showTimeoutMs_ = + currentMediaItem && currentMediaItem.mimeType === 'audio/*' ? + SHOW_TIMEOUT_MS_AUDIO : + this.showTimeoutMsVideo_; + } + const playWhenReady = playerState.playWhenReady; + const state = playerState.playbackState; + const isPlaying = playWhenReady && state === Player.PlaybackState.READY; + const userSeekedInBufferedRange = + discontinuityReason === Player.DiscontinuityReason.SEEK && isPlaying; + if (!isPlaying) { + this.show(); + } else if ((!this.wasPlaying_ && isPlaying) || userSeekedInBufferedRange) { + this.hideAfterTimeout(); + } + this.wasPlaying_ = isPlaying; + }); +}; + +/** Shows the player info view. */ +PlaybackInfoView.prototype.show = function () { + if (this.container_ != null) { + this.hideTimeout_.cancel(); + this.updateUi_(); + this.container_.style.display = 'block'; + this.startUpdateTimeout_(); + } +}; + +/** Hides the player info view. */ +PlaybackInfoView.prototype.hideAfterTimeout = function() { + if (this.container_ === null) { + return; + } + this.show(); + this.hideTimeout_.postDelayed(this.showTimeoutMs_).then(() => { + this.container_.style.display = 'none'; + this.updateTimeout_.cancel(); + }); +}; + +/** + * Sets the playback info view timeout. The playback info view is automatically + * hidden after this duration of time has elapsed without show() being called + * again. When playing streams with content type 'audio/*' the view is always + * displayed. + * + * @param {number} showTimeoutMs The duration in milliseconds. A non-positive + * value will cause the view to remain visible indefinitely. + */ +PlaybackInfoView.prototype.setShowTimeoutMs = function(showTimeoutMs) { + this.showTimeoutMs_ = showTimeoutMs; + this.showTimeoutMsVideo_ = showTimeoutMs; +}; + +/** + * Updates all UI components. + * + * @private + */ +PlaybackInfoView.prototype.updateUi_ = function () { + const elapsedTimeMs = this.player_.getCurrentPositionMs(); + const durationMs = this.player_.getDurationMs(); + if (this.elapsedTimeLabel_ !== null) { + this.updateDuration_(this.elapsedTimeLabel_, elapsedTimeMs, false); + } + if (this.durationLabel_ !== null) { + this.updateDuration_(this.durationLabel_, durationMs, true); + } + if (this.elapsedTimeBar_ !== null) { + this.updateProgressBar_(elapsedTimeMs, durationMs); + } +}; + +/** + * Adjust the progress bar indicating the elapsed time relative to the duration. + * + * @private + * @param {number} elapsedTimeMs The elapsed time in milliseconds. + * @param {number} durationMs The duration in milliseconds. + */ +PlaybackInfoView.prototype.updateProgressBar_ = + function(elapsedTimeMs, durationMs) { + if (elapsedTimeMs <= 0 || durationMs <= 0) { + this.elapsedTimeBar_.style.width = 0; + } else { + const widthPercentage = elapsedTimeMs / durationMs * 100; + this.elapsedTimeBar_.style.width = Math.min(100, widthPercentage) + '%'; + } +}; + +/** + * Updates the display value of the duration in the DOM formatted as hh:mm:ss. + * + * @private + * @param {!Element} element The element to update. + * @param {number} durationMs The duration in milliseconds. + * @param {boolean} hideZero If true values of zero and below are not displayed. + */ +PlaybackInfoView.prototype.updateDuration_ = + function (element, durationMs, hideZero) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + if (durationMs <= 0 && !hideZero) { + element.appendChild(dom.createDom(dom.TagName.SPAN, {}, + formatTimestampMsAsString(0))); + } else if (durationMs > 0) { + element.appendChild(dom.createDom(dom.TagName.SPAN, {}, + formatTimestampMsAsString(durationMs))); + } +}; + +/** + * Starts a repeating timeout that updates the UI every UPDATE_TIMEOUT_MS + * milliseconds. + * + * @private + */ +PlaybackInfoView.prototype.startUpdateTimeout_ = function() { + this.updateTimeout_.cancel(); + if (!this.player_.getPlayWhenReady() || + this.player_.getPlaybackState() !== Player.PlaybackState.READY) { + return; + } + this.updateTimeout_.postDelayed(UPDATE_TIMEOUT_MS).then(() => { + this.updateUi_(); + this.startUpdateTimeout_(); + }); +}; + +exports = PlaybackInfoView; diff --git a/cast_receiver_app/src/player.js b/cast_receiver_app/src/player.js new file mode 100644 index 0000000000..d7ffc58f4c --- /dev/null +++ b/cast_receiver_app/src/player.js @@ -0,0 +1,1522 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.Player'); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); +const ShakaError = goog.require('shaka.util.Error'); +const ShakaPlayer = goog.require('shaka.Player'); +const asserts = goog.require('goog.dom.asserts'); +const googArray = goog.require('goog.array'); +const safedom = goog.require('goog.dom.safe'); +const {ErrorMessages, ErrorCategory, PlaybackType, RepeatMode, getPlaybackType, UNKNOWN_ERROR} = goog.require('exoplayer.cast.constants'); +const {UuidComparator, createUuidComparator, log} = goog.require('exoplayer.cast.util'); +const {assert, fail} = goog.require('goog.asserts'); +const {clamp} = goog.require('goog.math'); + +/** + * Value indicating that no window index is currently set. + */ +const INDEX_UNSET = -1; + +/** + * Estimated time for processing the manifest after download in millisecconds. + * + * See: https://github.com/google/shaka-player/issues/1734 + */ +const MANIFEST_PROCESSING_ESTIMATE_MS = 350; + +/** + * Media element events to listen to. + * + * @enum {string} + */ +const MediaElementEvent = { + ERROR: 'error', + LOADED_DATA: 'loadeddata', + PAUSE: 'pause', + PLAYING: 'playing', + SEEKED: 'seeked', + SEEKING: 'seeking', + WAITING: 'waiting', +}; + +/** + * Shaka events to listen to. + * + * @enum {string} + */ +const ShakaEvent = { + ERROR: 'error', + STREAMING: 'streaming', + TRACKS_CHANGED: 'trackschanged', +}; + +/** + * ExoPlayer's playback states. + * + * @enum {string} + */ +const PlaybackState = { + IDLE: 'IDLE', + BUFFERING: 'BUFFERING', + READY: 'READY', + ENDED: 'ENDED', +}; + +/** + * ExoPlayer's position discontinuity reasons. + * + * @enum {string} + */ +const DiscontinuityReason = { + PERIOD_TRANSITION: 'PERIOD_TRANSITION', + SEEK: 'SEEK', +}; + +/** + * A dummy `MediaIteminfo` to be used while the actual period is not + * yet available. + * + * @const + * @type {!MediaItemInfo} + */ +const DUMMY_MEDIA_ITEM_INFO = Object.freeze({ + isSeekable: false, + isDynamic: true, + positionInFirstPeriodUs: 0, + defaultStartPositionUs: 0, + windowDurationUs: 0, + periods: [{ + id: 1, + durationUs: 0, + }], +}); + +/** + * The Player wraps a Shaka player and maintains a queue of media items. + * + * After construction the player is in `IDLE` state. Calling `#prepare` prepares + * the player with the queue item at the given window index and position. The + * state transitions to `BUFFERING`. When 'playWhenReady' is set to `true` + * playback start when the player becomes 'READY'. + * + * When the player needs to rebuffer the state goes to 'BUFFERING' and becomes + * 'READY' again when playback can be resumed. + * + * The state transitions to `ENDED` when playback reached the end of the last + * item in the queue, when the last item has been removed from the queue if + * `!IDLE`, or when `prepare` is called with an empty queue. Seeking makes the + * player transition away from `ENDED` again. + * + * When `#stop` is called or when a fatal playback error occurs, the player + * transition to `IDLE` state and needs to be prepared again to resume playback. + * + * `playWhenReady`, `repeatMode`, `shuffleModeEnabled` can be manipulated in any + * state, just as media items can be added, moved and removed. + * + * @constructor + * @param {!ShakaPlayer} shakaPlayer The shaka player to wrap. + * @param {!ConfigurationFactory} configurationFactory A factory to create a + * configuration for the Shaka player. + */ +const Player = function(shakaPlayer, configurationFactory) { + /** @private @const {?HTMLMediaElement} */ + this.videoElement_ = shakaPlayer.getMediaElement(); + /** @private @const {!ConfigurationFactory} */ + this.configurationFactory_ = configurationFactory; + /** @private @const {!Array} */ + this.playerListeners_ = []; + /** + * @private + * @const + * {?function(NetworkingEngine.RequestType, (?|null))} + */ + this.manifestResponseFilter_ = (type, response) => { + if (type === NetworkingEngine.RequestType.MANIFEST) { + setTimeout(() => { + this.updateWindowMediaItemInfo_(); + this.invalidate(); + }, MANIFEST_PROCESSING_ESTIMATE_MS); + } + }; + + /** @private {!ShakaPlayer} */ + this.shakaPlayer_ = shakaPlayer; + /** @private {boolean} */ + this.playWhenReady_ = false; + /** @private {boolean} */ + this.shuffleModeEnabled_ = false; + /** @private {!RepeatMode} */ + this.repeatMode_ = RepeatMode.OFF; + /** @private {!TrackSelectionParameters} */ + this.trackSelectionParameters_ = /** @type {!TrackSelectionParameters} */ ({ + preferredAudioLanguage: '', + preferredTextLanguage: '', + disabledTextTrackSelectionFlags: [], + selectUndeterminedTextLanguage: false, + }); + /** @private {number} */ + this.windowIndex_ = INDEX_UNSET; + /** @private {!Array} */ + this.queue_ = []; + /** @private {!Object} */ + this.queueUuidIndexMap_ = {}; + /** @private {!UuidComparator} */ + this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); + + /** @private {!PlaybackState} */ + this.playbackState_ = PlaybackState.IDLE; + /** @private {!MediaItemInfo} */ + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + /** @private {number} */ + this.windowPeriodIndex_ = 0; + /** @private {!Object} */ + this.mediaItemInfoMap_ = {}; + /** @private {?PlayerError} */ + this.playbackError_ = null; + /** @private {?DiscontinuityReason} */ + this.discontinuityReason_ = null; + /** @private {!Array} */ + this.shuffleOrder_ = []; + /** @private {number} */ + this.shuffleIndex_ = 0; + /** @private {!PlaybackType} */ + this.playbackType_ = PlaybackType.UNKNOWN; + /** @private {boolean} */ + this.isManifestFilterRegistered_ = false; + /** @private {?string} */ + this.uuidToPrepare_ = null; + + if (!this.shakaPlayer_ || !this.videoElement_) { + throw new Error('an instance of Shaka player with a media element ' + + 'attached to it needs to be passed to the constructor.'); + } + + /** @private @const {function(!Event)} */ + this.playbackStateListener_ = (ev) => { + log(['handle event: ', ev.type]); + let invalid = false; + switch (ev.type) { + case ShakaEvent.STREAMING: { + // Arrives once after prepare when the manifest is available. + const uuid = this.queue_[this.windowIndex_].uuid; + const cachedMediaItemInfo = this.mediaItemInfoMap_[uuid]; + if (!cachedMediaItemInfo || cachedMediaItemInfo.isDynamic) { + this.updateWindowMediaItemInfo_(); + if (this.windowMediaItemInfo_.isDynamic) { + this.registerManifestResponseFilter_(); + } + invalid = true; + } + break; + } + case ShakaEvent.TRACKS_CHANGED: { + // Arrives when tracks have changed either initially or at a period + // boundary. + const periods = this.windowMediaItemInfo_.periods; + const previousPeriodIndex = this.windowPeriodIndex_; + this.evaluateAndSetCurrentPeriod_(periods); + invalid = previousPeriodIndex !== this.windowPeriodIndex_; + if (periods.length && this.windowPeriodIndex_ > 0) { + // Player transitions to next period in multiperiod stream. + this.discontinuityReason_ = this.discontinuityReason_ || + DiscontinuityReason.PERIOD_TRANSITION; + invalid = true; + } + if (this.videoElement_.paused && this.playWhenReady_) { + this.videoElement_.play(); + } + break; + } + case MediaElementEvent.LOADED_DATA: { + // Arrives once when the first frame has been rendered. + if (this.playbackType_ === PlaybackType.VIDEO_ELEMENT) { + const uuid = this.queue_[this.windowIndex_].uuid; + let mediaItemInfo = this.mediaItemInfoMap_[uuid]; + if (!mediaItemInfo || mediaItemInfo.isDynamic) { + mediaItemInfo = this.buildMediaItemInfoFromElement_(); + if (mediaItemInfo !== null) { + this.mediaItemInfoMap_[uuid] = mediaItemInfo; + this.windowMediaItemInfo_ = mediaItemInfo; + } + } + this.evaluateAndSetCurrentPeriod_(mediaItemInfo.periods); + invalid = true; + } + if (this.videoElement_.paused && this.playWhenReady_) { + // Restart after automatic skip to next queue item. + this.videoElement_.play(); + } else if (this.videoElement_.paused) { + // If paused, the PLAYING event will not be fired, hence we transition + // to state READY right here. + this.playbackState_ = PlaybackState.READY; + invalid = true; + } + break; + } + case MediaElementEvent.WAITING: + case MediaElementEvent.SEEKING: { + // Arrives at a user seek or when re-buffering starts. + if (this.playbackState_ !== PlaybackState.BUFFERING) { + this.playbackState_ = PlaybackState.BUFFERING; + invalid = true; + } + break; + } + case MediaElementEvent.PLAYING: + case MediaElementEvent.SEEKED: { + // Arrives at the end of a user seek or after re-buffering. + if (this.playbackState_ !== PlaybackState.READY) { + this.playbackState_ = PlaybackState.READY; + invalid = true; + } + break; + } + case MediaElementEvent.PAUSE: { + // Detects end of media and either skips to next or transitions to ended + // state. + if (this.videoElement_.ended) { + let nextWindowIndex = this.getNextWindowIndex(); + if (nextWindowIndex !== INDEX_UNSET) { + this.seekToWindowInternal_(nextWindowIndex, undefined); + } else { + this.playbackState_ = PlaybackState.ENDED; + invalid = true; + } + } + break; + } + } + if (invalid) { + this.invalidate(); + } + }; + /** @private @const {function(!Event)} */ + this.mediaElementErrorHandler_ = (ev) => { + console.error('Media element error reported in handler'); + this.playbackError_ = !this.videoElement_.error ? UNKNOWN_ERROR : { + message: this.videoElement_.error.message, + code: this.videoElement_.error.code, + category: ErrorCategory.MEDIA_ELEMENT, + }; + this.playbackState_ = PlaybackState.IDLE; + this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? + this.queue_[this.windowIndex_].uuid : + null; + this.invalidate(); + }; + /** @private @const {function(!Event)} */ + this.shakaErrorHandler_ = (ev) => { + const shakaError = /** @type {!ShakaError} */ (ev['detail']); + if (shakaError.severity !== ShakaError.Severity.RECOVERABLE) { + this.fatalShakaError_(shakaError, 'Shaka error reported by error event'); + this.invalidate(); + } else { + console.error('Recoverable Shaka error reported in handler'); + } + }; + + this.shakaPlayer_.addEventListener( + ShakaEvent.STREAMING, this.playbackStateListener_); + this.shakaPlayer_.addEventListener( + ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); + + this.videoElement_.addEventListener( + MediaElementEvent.LOADED_DATA, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.WAITING, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.PLAYING, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.PAUSE, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.SEEKING, this.playbackStateListener_); + this.videoElement_.addEventListener( + MediaElementEvent.SEEKED, this.playbackStateListener_); + + // Attach error handlers. + this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); + this.videoElement_.addEventListener( + MediaElementEvent.ERROR, this.mediaElementErrorHandler_); +}; + +/** + * Adds a listener to the player. + * + * @param {function(!PlayerState)} listener The player listener. + */ +Player.prototype.addPlayerListener = function(listener) { + this.playerListeners_.push(listener); +}; + +/** + * Removes a listener. + * + * @param {function(!Object)} listener The player listener. + */ +Player.prototype.removePlayerListener = function(listener) { + for (let i = 0; i < this.playerListeners_.length; i++) { + if (this.playerListeners_[i] === listener) { + this.playerListeners_.splice(i, 1); + break; + } + } +}; + +/** + * Gets the current PlayerState. + * + * @return {!PlayerState} + */ +Player.prototype.getPlayerState = function() { + return this.buildPlayerState_(); +}; + +/** + * Sends the current playback state to clients. + */ +Player.prototype.invalidate = function() { + const playbackState = this.buildPlayerState_(); + for (let i = 0; i < this.playerListeners_.length; i++) { + this.playerListeners_[i](playbackState); + } +}; + +/** + * Get the audio tracks. + * + * @return {!Array} An array with the track names}. + */ +Player.prototype.getAudioTracks = function() { + return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? + this.shakaPlayer_.getAudioLanguages() : + []; +}; + +/** + * Gets the video tracks. + * + * @return {!Array} An array with the video tracks. + */ +Player.prototype.getVideoTracks = function() { + return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? + this.shakaPlayer_.getVariantTracks() : + []; +}; + +/** + * Gets the playback state. + * + * @return {!PlaybackState} The playback state. + */ +Player.prototype.getPlaybackState = function() { + return this.playbackState_; +}; + +/** + * Gets the playback error if any. + * + * @return {?Object} The playback error. + */ +Player.prototype.getPlaybackError = function() { + return this.playbackError_; +}; + +/** + * Gets the duration in milliseconds or a negative value if unknown. + * + * @return {number} The duration in milliseconds. + */ +Player.prototype.getDurationMs = function() { + return this.windowMediaItemInfo_ ? + this.windowMediaItemInfo_.windowDurationUs / 1000 : -1; +}; + +/** + * Gets the current position in milliseconds or a negative value if not known. + * + * @return {number} The current position in milliseconds. + */ +Player.prototype.getCurrentPositionMs = function() { + if (!this.videoElement_.currentTime) { + return 0; + } + return (this.videoElement_.currentTime * 1000) - + (this.windowMediaItemInfo_.positionInFirstPeriodUs / 1000); +}; + +/** + * Gets the current window index. + * + * @return {number} The current window index. + */ +Player.prototype.getCurrentWindowIndex = function() { + if (this.playbackState_ === PlaybackState.IDLE) { + return this.queueUuidIndexMap_[this.uuidToPrepare_ || ''] || 0; + } + return Math.max(0, this.windowIndex_); +}; + +/** + * Gets the media item of the current window or null if the queue is empty. + * + * @return {?MediaItem} The media item of the current window. + */ +Player.prototype.getCurrentMediaItem = function() { + return this.windowIndex_ >= 0 ? this.queue_[this.windowIndex_] : null; +}; + +/** + * Gets the media item info of the current window index or null if not yet + * available. + * + * @return {?MediaItemInfo} The current media item info or undefined. + */ +Player.prototype.getCurrentMediaItemInfo = function () { + return this.windowMediaItemInfo_; +}; + +/** + * Gets the text tracks. + * + * @return {!TextTrackList} The text tracks. + */ +Player.prototype.getTextTracks = function() { + return this.videoElement_.textTracks; +}; + +/** + * Gets whether the player should play when ready. + * + * @return {boolean} True when it plays when ready. + */ +Player.prototype.getPlayWhenReady = function() { + return this.playWhenReady_; +}; + +/** + * Sets whether to play when ready. + * + * @param {boolean} playWhenReady Whether to play when ready. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setPlayWhenReady = function(playWhenReady) { + if (this.playWhenReady_ === playWhenReady) { + return false; + } + this.playWhenReady_ = playWhenReady; + this.invalidate(); + if (this.playbackState_ === PlaybackState.IDLE || + this.playbackState_ === PlaybackState.ENDED) { + return true; + } + if (this.playWhenReady_) { + this.videoElement_.play(); + } else { + this.videoElement_.pause(); + } + return true; +}; + +/** + * Gets the repeat mode. + * + * @return {!RepeatMode} The repeat mode. + */ +Player.prototype.getRepeatMode = function() { + return this.repeatMode_; +}; + +/** + * Sets the repeat mode. Must be a value of the enum Player.RepeatMode. + * + * @param {!RepeatMode} mode The repeat mode. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setRepeatMode = function(mode) { + if (this.repeatMode_ === mode) { + return false; + } + if (mode === Player.RepeatMode.OFF || + mode === Player.RepeatMode.ONE || + mode === Player.RepeatMode.ALL) { + this.repeatMode_ = mode; + } else { + throw new Error('illegal repeat mode: ' + mode); + } + this.invalidate(); + return true; +}; + +/** + * Enables or disables the shuffle mode. + * + * @param {boolean} enabled Whether the shuffle mode is enabled or not. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setShuffleModeEnabled = function(enabled) { + if (this.shuffleModeEnabled_ === enabled) { + return false; + } + this.shuffleModeEnabled_ = enabled; + this.invalidate(); + return true; +}; + +/** + * Sets the track selection parameters. + * + * @param {!TrackSelectionParameters} trackSelectionParameters The parameters. + * @return {boolean} Whether calling this method causes a change of the player + * state. + */ +Player.prototype.setTrackSelectionParameters = function( + trackSelectionParameters) { + this.trackSelectionParameters_ = trackSelectionParameters; + /** @type {!PlayerConfiguration} */ + const configuration = /** @type {!PlayerConfiguration} */ ({}); + this.configurationFactory_.mapLanguageConfiguration( + trackSelectionParameters, configuration); + /** @type {!PlayerConfiguration} */ + const currentConfiguration = this.shakaPlayer_.getConfiguration(); + /** @type {boolean} */ + let isStateChange = false; + if (currentConfiguration.preferredAudioLanguage !== + configuration.preferredAudioLanguage) { + this.shakaPlayer_.selectAudioLanguage(configuration.preferredAudioLanguage); + isStateChange = true; + } + if (currentConfiguration.preferredTextLanguage !== + configuration.preferredTextLanguage) { + this.shakaPlayer_.selectTextLanguage(configuration.preferredTextLanguage); + isStateChange = true; + } + return isStateChange; +}; + +/** + * Gets the previous window index or a negative number if no item previous to + * the current item is available. + * + * @return {number} The previous window index or a negative number if the + * current item is the first item. + */ +Player.prototype.getPreviousWindowIndex = function() { + if (this.playbackType_ === PlaybackType.UNKNOWN) { + return INDEX_UNSET; + } + switch (this.repeatMode_) { + case RepeatMode.ONE: + return this.windowIndex_; + case RepeatMode.ALL: + if (this.shuffleModeEnabled_) { + const previousIndex = this.shuffleIndex_ > 0 ? + this.shuffleIndex_ - 1 : this.queue_.length - 1; + return this.shuffleOrder_[previousIndex]; + } else { + const previousIndex = this.windowIndex_ > 0 ? + this.windowIndex_ - 1 : this.queue_.length - 1; + return previousIndex; + } + break; + case RepeatMode.OFF: + if (this.shuffleModeEnabled_) { + const previousIndex = this.shuffleIndex_ - 1; + return previousIndex < 0 ? -1 : this.shuffleOrder_[previousIndex]; + } else { + const previousIndex = this.windowIndex_ - 1; + return previousIndex < 0 ? -1 : previousIndex; + } + break; + default: + throw new Error('illegal state of repeat mode: ' + this.repeatMode_); + } +}; + +/** + * Gets the next window index or a negative number if the current item is the + * last item. + * + * @return {number} The next window index or a negative number if the current + * item is the last item. + */ +Player.prototype.getNextWindowIndex = function() { + if (this.playbackType_ === PlaybackType.UNKNOWN) { + return INDEX_UNSET; + } + switch (this.repeatMode_) { + case RepeatMode.ONE: + return this.windowIndex_; + case RepeatMode.ALL: + if (this.shuffleModeEnabled_) { + const nextIndex = (this.shuffleIndex_ + 1) % this.queue_.length; + return this.shuffleOrder_[nextIndex]; + } else { + return (this.windowIndex_ + 1) % this.queue_.length; + } + break; + case RepeatMode.OFF: + if (this.shuffleModeEnabled_) { + const nextIndex = this.shuffleIndex_ + 1; + return nextIndex < this.shuffleOrder_.length ? + this.shuffleOrder_[nextIndex] : -1; + } else { + const nextIndex = this.windowIndex_ + 1; + return nextIndex < this.queue_.length ? nextIndex : -1; + } + break; + default: + throw new Error('illegal state of repeat mode: ' + this.repeatMode_); + } +}; + +/** + * Gets whether the current window is seekable. + * + * @return {boolean} True if seekable. + */ +Player.prototype.isCurrentWindowSeekable = function() { + return !!this.videoElement_.seekable; +}; + +/** + * Seeks to the positionMs of the media item with the given uuid. + * + * @param {string} uuid The uuid of the media item to seek to. + * @param {number|undefined} positionMs The position in milliseconds to seek to. + * @return {boolean} True if a seek operation has been processed, false + * otherwise. + */ +Player.prototype.seekToUuid = function(uuid, positionMs) { + if (this.playbackState_ === PlaybackState.IDLE) { + this.uuidToPrepare_ = uuid; + this.videoElement_.currentTime = + this.getPosition_(positionMs, INDEX_UNSET) / 1000; + this.invalidate(); + return true; + } + const windowIndex = this.queueUuidIndexMap_[uuid]; + if (windowIndex !== undefined) { + positionMs = this.getPosition_(positionMs, windowIndex); + this.discontinuityReason_ = DiscontinuityReason.SEEK; + this.seekToWindowInternal_(windowIndex, positionMs); + return true; + } + return false; +}; + +/** + * Seeks to the positionMs of the given window. + * + * The index must be a valid index of the current queue, else this method does + * nothing. + * + * @param {number} windowIndex The index of the window to seek to. + * @param {number|undefined} positionMs The position to seek to within the + * window. + */ +Player.prototype.seekToWindow = function(windowIndex, positionMs) { + if (windowIndex < 0 || windowIndex >= this.queue_.length) { + return; + } + this.seekToUuid(this.queue_[windowIndex].uuid, positionMs); +}; + +/** + * Gets the number of media items in the queue. + * + * @return {number} The size of the queue. + */ +Player.prototype.getQueueSize = function() { + return this.queue_.length; +}; + +/** + * Adds an array of items at the given index of the queue. + * + * Items are expected to have been validated with `validation#validateMediaItem` + * or `validation#validateMediaItems` before being passed to this method. + * + * @param {number} index The index where to insert the media item. + * @param {!Array} mediaItems The media items. + * @param {!Array|undefined} shuffleOrder The new shuffle order. + * @return {number} The number of added items. + */ +Player.prototype.addQueueItems = function(index, mediaItems, shuffleOrder) { + if (index < 0 || mediaItems.length === 0) { + return 0; + } + let addedItemCount = 0; + index = Math.min(this.queue_.length, index); + mediaItems.forEach((itemToAdd) => { + if (this.queueUuidIndexMap_[itemToAdd.uuid] === undefined) { + this.queue_.splice(index + addedItemCount, 0, itemToAdd); + this.queueUuidIndexMap_[itemToAdd.uuid] = index + addedItemCount; + addedItemCount++; + } + }); + if (addedItemCount === 0) { + return 0; + } + this.buildUuidIndexMap_(index + addedItemCount); + this.setShuffleOrder_(shuffleOrder); + if (this.queue_.length === addedItemCount) { + this.windowIndex_ = 0; + this.updateShuffleIndex_(); + } else if ( + index <= this.windowIndex_ && + this.playbackType_ !== PlaybackType.UNKNOWN) { + this.windowIndex_ += mediaItems.length; + this.updateShuffleIndex_(); + } + this.invalidate(); + return addedItemCount; +}; + +/** + * Removes the queue items with the given uuids. + * + * @param {!Array} uuids The uuids of the queue items to remove. + * @return {number} The number of items removed from the queue. + */ +Player.prototype.removeQueueItems = function(uuids) { + let currentWindowRemoved = false; + let lowestIndexRemoved = this.queue_.length - 1; + const initialQueueSize = this.queue_.length; + // Sort in descending order to start removing from the end. + uuids = uuids.sort(this.uuidComparator_); + uuids.forEach((uuid) => { + const indexToRemove = this.queueUuidIndexMap_[uuid]; + if (indexToRemove === undefined) { + return; + } + // Remove the item from the queue. + this.queue_.splice(indexToRemove, 1); + // Remove the corresponding media item info. + delete this.mediaItemInfoMap_[uuid]; + // Remove the mapping to the window index. + delete this.queueUuidIndexMap_[uuid]; + lowestIndexRemoved = Math.min(lowestIndexRemoved, indexToRemove); + currentWindowRemoved = + currentWindowRemoved || indexToRemove === this.windowIndex_; + // The window index needs to be decreased when the item which has been + // removed was before the current item, when the current item at the last + // position has been removed, or when the queue has been emptied. + if (indexToRemove < this.windowIndex_ || + (indexToRemove === this.windowIndex_ && + indexToRemove === this.queue_.length) || + this.queue_.length === 0) { + this.windowIndex_--; + } + // Adjust the shuffle order. + let shuffleIndexToRemove; + this.shuffleOrder_.forEach((windowIndex, index) => { + if (windowIndex > indexToRemove) { + // Decrease the index in the shuffle order. + this.shuffleOrder_[index]--; + } else if (windowIndex === indexToRemove) { + // Recall index for removal after traversing. + shuffleIndexToRemove = index; + } + }); + // Remove the shuffle order entry of the removed item. + this.shuffleOrder_.splice(shuffleIndexToRemove, 1); + }); + const removedItemsCount = initialQueueSize - this.queue_.length; + if (removedItemsCount === 0) { + return 0; + } + this.updateShuffleIndex_(); + this.buildUuidIndexMap_(lowestIndexRemoved); + if (currentWindowRemoved) { + if (this.queue_.length === 0) { + this.playbackState_ = this.playbackState_ === PlaybackState.IDLE ? + PlaybackState.IDLE : + PlaybackState.ENDED; + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + this.windowPeriodIndex_ = 0; + this.videoElement_.currentTime = 0; + this.uuidToPrepare_ = null; + this.unregisterManifestResponseFilter_(); + this.unload_(/** reinitialiseMediaSource= */ true); + } else if (this.windowIndex_ >= 0) { + const windowIndexToPrepare = this.windowIndex_; + this.windowIndex_ = INDEX_UNSET; + this.seekToWindowInternal_(windowIndexToPrepare, undefined); + return removedItemsCount; + } + } + this.invalidate(); + return removedItemsCount; +}; + +/** + * Move the queue item with the given id to the given position. + * + * @param {string} uuid The uuid of the queue item to move. + * @param {number} to The position to move the item to. + * @param {!Array|undefined} shuffleOrder The new shuffle order. + * @return {boolean} Whether the item has been moved. + */ +Player.prototype.moveQueueItem = function(uuid, to, shuffleOrder) { + if (to < 0 || to >= this.queue_.length) { + return false; + } + const windowIndex = this.queueUuidIndexMap_[uuid]; + if (windowIndex === undefined) { + return false; + } + const itemMoved = this.moveInQueue_(windowIndex, to); + if (itemMoved) { + this.setShuffleOrder_(shuffleOrder); + this.invalidate(); + } + return itemMoved; +}; + +/** + * Prepares the player at the current window index and position. + * + * The playback state immediately transitions to `BUFFERING`. If the queue + * is empty the player transitions to `ENDED`. + */ +Player.prototype.prepare = function() { + if (this.queue_.length === 0) { + this.uuidToPrepare_ = null; + this.playbackState_ = PlaybackState.ENDED; + this.invalidate(); + return; + } + if (this.uuidToPrepare_) { + this.windowIndex_ = + this.queueUuidIndexMap_[this.uuidToPrepare_] || INDEX_UNSET; + this.uuidToPrepare_ = null; + } + this.windowIndex_ = clamp(this.windowIndex_, 0, this.queue_.length - 1); + this.prepare_(this.getCurrentPositionMs()); + this.invalidate(); +}; + +/** + * Stops the player. + * + * Calling this method causes the player to transition into `IDLE` state. + * If `reset` is `true` the player is reset to the initial state of right + * after construction. If `reset` is `false`, the media queue is preserved + * and calling `prepare()` results in resuming the player state to what it + * was before calling `#stop(false)`. + * + * @param {boolean} reset Whether the state should be reset. + * @return {!Promise} A promise which resolves after async unload + * tasks have finished. + */ +Player.prototype.stop = function(reset) { + this.playbackState_ = PlaybackState.IDLE; + this.playbackError_ = null; + this.discontinuityReason_ = null; + this.unregisterManifestResponseFilter_(); + this.uuidToPrepare_ = this.uuidToPrepare_ || (this.queue_[this.windowIndex_] ? + this.queue_[this.windowIndex_].uuid : + null); + if (reset) { + this.uuidToPrepare_ = null; + this.queue_ = []; + this.queueUuidIndexMap_ = {}; + this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); + this.windowIndex_ = INDEX_UNSET; + this.mediaItemInfoMap_ = {}; + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + this.windowPeriodIndex_ = 0; + this.videoElement_.currentTime = 0; + this.shuffleOrder_ = []; + this.shuffleIndex_ = 0; + } + this.invalidate(); + return this.unload_(/** reinitialiseMediaSource= */ !reset); +}; + +/** + * Resets player and media element. + * + * @private + * @param {boolean} reinitialiseMediaSource Whether the media source should be + * reinitialized. + * @return {!Promise} A promise which resolves after async unload + * tasks have finished. + */ +Player.prototype.unload_ = function(reinitialiseMediaSource) { + const playbackTypeToUnload = this.playbackType_; + this.playbackType_ = PlaybackType.UNKNOWN; + switch (playbackTypeToUnload) { + case PlaybackType.VIDEO_ELEMENT: + this.videoElement_.removeAttribute('src'); + this.videoElement_.load(); + return Promise.resolve(); + case PlaybackType.SHAKA_PLAYER: + return new Promise((resolve, reject) => { + this.shakaPlayer_.unload(reinitialiseMediaSource) + .then(resolve) + .catch(resolve); + }); + default: + return Promise.resolve(); + } +}; + +/** + * Releases the current Shaka instance and create a new one. + * + * This function should only be called if the Shaka instance is out of order due + * to https://github.com/google/shaka-player/issues/1785. It assumes the current + * Shaka instance has fallen into a state in which promises returned by + * `shakaPlayer.load` and `shakaPlayer.unload` do not resolve nor are they + * rejected anymore. + * + * @private + */ +Player.prototype.replaceShaka_ = function() { + // Remove all listeners. + this.shakaPlayer_.removeEventListener( + ShakaEvent.STREAMING, this.playbackStateListener_); + this.shakaPlayer_.removeEventListener( + ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); + this.shakaPlayer_.removeEventListener( + ShakaEvent.ERROR, this.shakaErrorHandler_); + // Unregister response filter if any. + this.unregisterManifestResponseFilter_(); + // Unload the old instance. + this.shakaPlayer_.unload(false); + // Reset video element. + this.videoElement_.removeAttribute('src'); + this.videoElement_.load(); + // Create a new instance and add listeners. + this.shakaPlayer_ = new ShakaPlayer(this.videoElement_); + this.shakaPlayer_.addEventListener( + ShakaEvent.STREAMING, this.playbackStateListener_); + this.shakaPlayer_.addEventListener( + ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); + this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); +}; + +/** + * Moves a queue item within the queue. + * + * @private + * @param {number} from The initial position. + * @param {number} to The position to move the item to. + * @return {boolean} Whether the item has been moved. + */ +Player.prototype.moveInQueue_ = function(from, to) { + if (from < 0 || to < 0 + || from >= this.queue_.length || to >= this.queue_.length + || from === to) { + return false; + } + this.queue_.splice(to, 0, this.queue_.splice(from, 1)[0]); + this.buildUuidIndexMap_(Math.min(from, to)); + if (from === this.windowIndex_) { + this.windowIndex_ = to; + } else if (from > this.windowIndex_ && to <= this.windowIndex_) { + this.windowIndex_++; + } else if (from < this.windowIndex_ && to >= this.windowIndex_) { + this.windowIndex_--; + } + return true; +}; + +/** + * Shuffles the queue. + * + * @private + */ +Player.prototype.shuffle_ = function() { + this.shuffleOrder_ = this.queue_.map((item, index) => index); + googArray.shuffle(this.shuffleOrder_); + this.updateShuffleIndex_(); +}; + +/** + * Sets the new shuffle order. + * + * @private + * @param {!Array|undefined} shuffleOrder The new shuffle order. + */ +Player.prototype.setShuffleOrder_ = function(shuffleOrder) { + if (shuffleOrder && this.queue_.length === shuffleOrder.length) { + this.shuffleOrder_ = shuffleOrder; + this.updateShuffleIndex_(); + } else if (this.shuffleOrder_.length !== this.queue_.length) { + this.shuffle_(); + } +}; + +/** + * Updates the shuffle order to point to the current window index. + * + * @private + */ +Player.prototype.updateShuffleIndex_ = function() { + this.shuffleIndex_ = + this.shuffleOrder_.findIndex((idx) => idx === this.windowIndex_); +}; + +/** + * Builds the `queueUuidIndexMap` using the uuid of a media item as the key and + * the window index as the value of an entry. + * + * @private + * @param {number} startPosition The window index to start updating at. + */ +Player.prototype.buildUuidIndexMap_ = function(startPosition) { + for (let i = startPosition; i < this.queue_.length; i++) { + this.queueUuidIndexMap_[this.queue_[i].uuid] = i; + } +}; + +/** + * Gets the default position of the current window. + * + * @private + * @return {number} The default position of the current window. + */ +Player.prototype.getDefaultPosition_ = function() { + return this.windowMediaItemInfo_.defaultStartPositionUs; +}; + +/** + * Checks whether the given position is buffered. + * + * @private + * @param {number} positionMs The position to check. + * @return {boolean} true if the media data of the current position is buffered. + */ +Player.prototype.isBuffered_ = function(positionMs) { + const ranges = this.videoElement_.buffered; + for (let i = 0; i < ranges.length; i++) { + const start = ranges.start(i) * 1000; + const end = ranges.end(i) * 1000; + if (start <= positionMs && positionMs <= end) { + return true; + } + } + return false; +}; + +/** + * Seeks to the positionMs of the given window. + * + * To signal a user seek, callers are expected to set the discontinuity reason + * to `DiscontinuityReason.SEEK` before calling this method. If not set this + * method may set the `DiscontinuityReason.PERIOD_TRANSITION` in case the + * `windowIndex` changes. + * + * @private + * @param {number} windowIndex The non-negative index of the window to seek to. + * @param {number|undefined} positionMs The position to seek to within the + * window. If undefined it seeks to the default position of the window. + */ +Player.prototype.seekToWindowInternal_ = function(windowIndex, positionMs) { + const windowChanges = this.windowIndex_ !== windowIndex; + // Update window index and position in any case. + this.windowIndex_ = Math.max(0, windowIndex); + this.updateShuffleIndex_(); + const seekPositionMs = this.getPosition_(positionMs, windowIndex); + this.videoElement_.currentTime = seekPositionMs / 1000; + + // IDLE or ENDED with empty queue. + if (this.playbackState_ === PlaybackState.IDLE || this.queue_.length === 0) { + // Do nothing but report the change in window index and position. + this.invalidate(); + return; + } + + // Prepare for a seek to another window or when in ENDED state whilst the + // queue is not empty but prepare has not been called yet. + if (windowChanges || this.playbackType_ === PlaybackType.UNKNOWN) { + // Reset and prepare. + this.unregisterManifestResponseFilter_(); + this.discontinuityReason_ = + this.discontinuityReason_ || DiscontinuityReason.PERIOD_TRANSITION; + this.prepare_(seekPositionMs); + this.invalidate(); + return; + } + + // Sync playWhenReady with video element after ENDED state. + if (this.playbackState_ === PlaybackState.ENDED && this.playWhenReady_) { + this.videoElement_.play(); + return; + } + + // A seek within the current window when READY or BUFFERING. + this.playbackState_ = this.isBuffered_(seekPositionMs) ? + PlaybackState.READY : + PlaybackState.BUFFERING; + this.invalidate(); +}; + +/** + * Prepares the player at the current window index and the given + * `startPositionMs`. + * + * Calling this method resets the media item information, transitions to + * 'BUFFERING', prepares either the plain video element for progressive + * media, or the Shaka player for adaptive media. + * + * Media items are mapped by media type to a `PlaybackType`s in + * `exoplayer.cast.constants.SupportedMediaTypes`. Unsupported mime types will + * cause the player to transition to the `IDLE` state. + * + * Items in the queue are expected to have been validated with + * `validation#validateMediaItem` or `validation#validateMediaItems`. If this is + * not the case this method might throw an Assertion exception. + * + * @private + * @param {number} startPositionMs The position at which to start playback. + * @throws {!AssertionException} In case an unvalidated item can't be mapped to + * a supported playback type. + */ +Player.prototype.prepare_ = function(startPositionMs) { + const mediaItem = this.queue_[this.windowIndex_]; + const windowUuid = this.queue_[this.windowIndex_].uuid; + const mediaItemInfo = this.mediaItemInfoMap_[windowUuid]; + if (mediaItemInfo && !mediaItemInfo.isDynamic) { + // Do reuse if not dynamic. + this.windowMediaItemInfo_ = mediaItemInfo; + } else { + // Use the dummy info until manifest/data available. + this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; + this.mediaItemInfoMap_[windowUuid] = DUMMY_MEDIA_ITEM_INFO; + } + this.windowPeriodIndex_ = 0; + this.playbackType_ = getPlaybackType(mediaItem.mimeType); + this.playbackState_ = PlaybackState.BUFFERING; + const uri = mediaItem.media.uri; + switch (this.playbackType_) { + case PlaybackType.VIDEO_ELEMENT: + this.videoElement_.currentTime = startPositionMs / 1000; + this.shakaPlayer_.unload(false) + .then(() => { + this.setMediaElementSrc(uri); + this.videoElement_.currentTime = startPositionMs / 1000; + }) + .catch((error) => { + // Let's still try. We actually don't need Shaka right now. + this.setMediaElementSrc(uri); + this.videoElement_.currentTime = startPositionMs / 1000; + console.error('Shaka error while unloading', error); + }); + break; + case PlaybackType.SHAKA_PLAYER: + this.shakaPlayer_.configure( + this.configurationFactory_.createConfiguration( + mediaItem, this.trackSelectionParameters_)); + this.shakaPlayer_.load(uri, startPositionMs / 1000).catch((error) => { + const shakaError = /** @type {!ShakaError} */ (error); + if (shakaError.severity !== ShakaError.Severity.RECOVERABLE && + shakaError.code !== ShakaError.Code.LOAD_INTERRUPTED) { + this.fatalShakaError_(shakaError, 'loading failed for uri: ' + uri); + this.invalidate(); + } else { + console.error('Recoverable Shaka error while loading', shakaError); + } + }); + break; + default: + fail('unknown playback type for mime type: ' + mediaItem.mimeType); + } +}; + +/** + * Sets the uri to the `src` attribute of the media element in a safe way. + * + * @param {string} uri The uri to set as the value of the `src` attribute. + */ +Player.prototype.setMediaElementSrc = function(uri) { + safedom.setVideoSrc( + asserts.assertIsHTMLVideoElement(this.videoElement_), uri); +}; + +/** + * Handles a fatal Shaka error by setting the playback error, transitioning to + * state `IDLE` and setting the playback type to `UNKNOWN`. Player needs to be + * reprepared after calling this method. + * + * @private + * @param {!ShakaError} shakaError The error. + * @param {string|undefined} customMessage A custom message. + */ +Player.prototype.fatalShakaError_ = function(shakaError, customMessage) { + this.playbackState_ = PlaybackState.IDLE; + this.playbackType_ = PlaybackType.UNKNOWN; + this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? + this.queue_[this.windowIndex_].uuid : + null; + if (typeof shakaError.severity === 'undefined') { + // Not a Shaka error. We need to assume the worst case. + this.replaceShaka_(); + this.playbackError_ = /** @type {!PlayerError} */ ({ + message: ErrorMessages.UNKNOWN_FATAL_ERROR, + code: -1, + category: ErrorCategory.FATAL_SHAKA_ERROR, + }); + } else { + // A critical ShakaError. Can be recovered from by calling prepare. + this.playbackError_ = /** @type {!PlayerError} */ ({ + message: customMessage || shakaError.message || + ErrorMessages.SHAKA_UNKNOWN_ERROR, + code: shakaError.code, + category: shakaError.category, + }); + } + console.error('caught shaka load error', shakaError); +}; + +/** + * Gets the position to use. If `undefined` or `null` is passed as argument the + * default start position of the media item info of the given windowIndex is + * returned. + * + * @private + * @param {?number|undefined} positionMs The position in milliseconds, + * `undefined` or `null`. + * @param {number} windowIndex The window index for which to evaluate the + * position. + * @return {number} The position to use in milliseconds. + */ +Player.prototype.getPosition_ = function(positionMs, windowIndex) { + if (positionMs !== undefined) { + return Math.max(0, positionMs); + } + const windowUuid = assert(this.queue_[windowIndex]).uuid; + const mediaItemInfo = + this.mediaItemInfoMap_[windowUuid] || DUMMY_MEDIA_ITEM_INFO; + return mediaItemInfo.defaultStartPositionUs; +}; + +/** + * Refreshes the media item info of the current window. + * + * @private + */ +Player.prototype.updateWindowMediaItemInfo_ = function() { + this.windowMediaItemInfo_ = this.buildMediaItemInfo_(); + if (this.windowMediaItemInfo_) { + const mediaItem = this.queue_[this.windowIndex_]; + this.mediaItemInfoMap_[mediaItem.uuid] = this.windowMediaItemInfo_; + this.evaluateAndSetCurrentPeriod_(this.windowMediaItemInfo_.periods); + } +}; + +/** + * Evaluates the current period and stores it in a member variable. + * + * @private + * @param {!Array} periods The periods of the current mediaItem. + */ +Player.prototype.evaluateAndSetCurrentPeriod_ = function(periods) { + const positionUs = this.getCurrentPositionMs() * 1000; + let positionInWindowUs = 0; + periods.some((period, i) => { + positionInWindowUs += period.durationUs; + if (positionUs < positionInWindowUs) { + this.windowPeriodIndex_ = i; + return true; + } + return false; + }); +}; + +/** + * Registers a response filter which is notified when a manifest has been + * downloaded. + * + * @private + */ +Player.prototype.registerManifestResponseFilter_ = function() { + if (this.isManifestFilterRegistered_) { + return; + } + this.shakaPlayer_.getNetworkingEngine().registerResponseFilter( + this.manifestResponseFilter_); + this.isManifestFilterRegistered_ = true; +}; + +/** + * Unregisters the manifest response filter. + * + * @private + */ +Player.prototype.unregisterManifestResponseFilter_ = function() { + if (this.isManifestFilterRegistered_) { + this.shakaPlayer_.getNetworkingEngine().unregisterResponseFilter( + this.manifestResponseFilter_); + this.isManifestFilterRegistered_ = false; + } +}; + +/** + * Builds a MediaItemInfo from the media element. + * + * @private + * @return {!MediaItemInfo} A media item info. + */ +Player.prototype.buildMediaItemInfoFromElement_ = function() { + const durationUs = this.videoElement_.duration * 1000 * 1000; + return /** @type {!MediaItemInfo} */ ({ + isSeekable: !!this.videoElement_.seekable, + isDynamic: false, + positionInFirstPeriodUs: 0, + defaultStartPositionUs: 0, + windowDurationUs: durationUs, + periods: [{ + id: 0, + durationUs: durationUs, + }], + }); +}; + +/** + * Builds a MediaItemInfo from the manifest or null if no manifest is available. + * + * @private + * @return {!MediaItemInfo} + */ +Player.prototype.buildMediaItemInfo_ = function() { + const manifest = this.shakaPlayer_.getManifest(); + if (manifest === null) { + return DUMMY_MEDIA_ITEM_INFO; + } + const timeline = manifest.presentationTimeline; + const isDynamic = timeline.isLive(); + const windowStartUs = isDynamic ? + timeline.getSeekRangeStart() * 1000 * 1000 : + timeline.getSegmentAvailabilityStart() * 1000 * 1000; + const windowDurationUs = isDynamic ? + (timeline.getSeekRangeEnd() - timeline.getSeekRangeStart()) * 1000 * + 1000 : + timeline.getDuration() * 1000 * 1000; + const defaultStartPositionUs = isDynamic ? + timeline.getSeekRangeEnd() * 1000 * 1000 : + timeline.getSegmentAvailabilityStart() * 1000 * 1000; + + const periods = []; + let previousStartTimeUs = 0; + let positionInFirstPeriodUs = 0; + manifest.periods.forEach((period, index) => { + const startTimeUs = period.startTime * 1000 * 1000; + periods.push({ + id: Math.floor(startTimeUs), + }); + if (index > 0) { + // calculate duration of previous period + periods[index - 1].durationUs = startTimeUs - previousStartTimeUs; + if (previousStartTimeUs <= windowStartUs && windowStartUs < startTimeUs) { + positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; + } + } + previousStartTimeUs = startTimeUs; + }); + // calculate duration of last period + if (periods.length) { + const lastPeriodDurationUs = + isDynamic ? Infinity : windowDurationUs - previousStartTimeUs; + periods.slice(-1)[0].durationUs = lastPeriodDurationUs; + if (previousStartTimeUs <= windowStartUs) { + positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; + } + } + return /** @type {!MediaItemInfo} */ ({ + windowDurationUs: Math.floor(windowDurationUs), + defaultStartPositionUs: Math.floor(defaultStartPositionUs), + isSeekable: this.videoElement_ ? !!this.videoElement_.seekable : false, + positionInFirstPeriodUs: Math.floor(positionInFirstPeriodUs), + isDynamic: isDynamic, + periods: periods, + }); +}; + +/** + * Builds the player state message. + * + * @private + * @return {!PlayerState} The player state. + */ +Player.prototype.buildPlayerState_ = function() { + const playerState = { + playbackState: this.getPlaybackState(), + playbackParameters: { + speed: 1, + pitch: 1, + skipSilence: false, + }, + playbackPosition: this.buildPlaybackPosition_(), + playWhenReady: this.getPlayWhenReady(), + windowIndex: this.getCurrentWindowIndex(), + windowCount: this.queue_.length, + audioTracks: this.getAudioTracks() || [], + videoTracks: this.getVideoTracks(), + repeatMode: this.repeatMode_, + shuffleModeEnabled: this.shuffleModeEnabled_, + mediaQueue: this.queue_.slice(), + mediaItemsInfo: this.mediaItemInfoMap_, + shuffleOrder: this.shuffleOrder_, + sequenceNumber: -1, + }; + if (this.playbackError_) { + playerState.error = this.playbackError_; + this.playbackError_ = null; + } + return playerState; +}; + +/** + * Builds the playback position. Returns null if all properties of the playback + * position are empty. + * + * @private + * @return {?PlaybackPosition} The playback position. + */ +Player.prototype.buildPlaybackPosition_ = function() { + if ((this.playbackState_ === PlaybackState.IDLE && !this.uuidToPrepare_) || + this.playbackState_ === PlaybackState.ENDED && this.queue_.length === 0) { + this.discontinuityReason_ = null; + return null; + } + /** @type {!PlaybackPosition} */ + const playbackPosition = { + positionMs: this.getCurrentPositionMs(), + uuid: this.uuidToPrepare_ || this.queue_[this.windowIndex_].uuid, + periodId: this.windowMediaItemInfo_.periods[this.windowPeriodIndex_].id, + discontinuityReason: null, + }; + if (this.discontinuityReason_ !== null) { + playbackPosition.discontinuityReason = this.discontinuityReason_; + this.discontinuityReason_ = null; + } + return playbackPosition; +}; + +exports = Player; +exports.RepeatMode = RepeatMode; +exports.PlaybackState = PlaybackState; +exports.DiscontinuityReason = DiscontinuityReason; +exports.DUMMY_MEDIA_ITEM_INFO = DUMMY_MEDIA_ITEM_INFO; diff --git a/cast_receiver_app/src/timeout.js b/cast_receiver_app/src/timeout.js new file mode 100644 index 0000000000..e5df5ec2f4 --- /dev/null +++ b/cast_receiver_app/src/timeout.js @@ -0,0 +1,68 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.Timeout'); + +/** + * A timeout which can be cancelled. + */ +class Timeout { + constructor() { + /** @private {?number} */ + this.timeout_ = null; + } + /** + * Returns a promise which resolves when the duration of time defined by + * delayMs has elapsed and cancel() has not been called earlier. + * + * If the timeout is already set, the former timeout is cancelled and a new + * one is started. + * + * @param {number} delayMs The delay after which to resolve or a non-positive + * value if it should never resolve. + * @return {!Promise} Resolves after the given delayMs or never + * for a non-positive delay. + */ + postDelayed(delayMs) { + this.cancel(); + return new Promise((resolve, reject) => { + if (delayMs <= 0) { + return; + } + this.timeout_ = setTimeout(() => { + if (this.timeout_) { + this.timeout_ = null; + resolve(); + } + }, delayMs); + }); + } + + /** Cancels the timeout. */ + cancel() { + if (this.timeout_) { + clearTimeout(this.timeout_); + this.timeout_ = null; + } + } + + /** @return {boolean} true if the timeout is currently ongoing. */ + isOngoing() { + return this.timeout_ !== null; + } +} + +exports = Timeout; diff --git a/cast_receiver_app/src/util.js b/cast_receiver_app/src/util.js new file mode 100644 index 0000000000..75afd9e5d3 --- /dev/null +++ b/cast_receiver_app/src/util.js @@ -0,0 +1,62 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.util'); + +/** + * Indicates whether the logging is turned on. + */ +const enableLogging = true; + +/** + * Logs to the console if logging enabled. + * + * @param {!Array<*>} statements The log statements to be logged. + */ +const log = function(statements) { + if (enableLogging) { + console.log.apply(console, statements); + } +}; + +/** + * A comparator function for uuids. + * + * @typedef {function(string,string):number} + */ +let UuidComparator; + +/** + * Creates a comparator function which sorts uuids in descending order by the + * corresponding index of the given map. + * + * @param {!Object} uuidIndexMap The map with uuids as the key + * and the window index as the value. + * @return {!UuidComparator} The comparator for sorting. + */ +const createUuidComparator = function(uuidIndexMap) { + return (a, b) => { + const indexA = uuidIndexMap[a] || -1; + const indexB = uuidIndexMap[b] || -1; + return indexB - indexA; + }; +}; + +exports = { + log, + createUuidComparator, + UuidComparator, +}; diff --git a/cast_receiver_app/test/caf_bootstrap.js b/cast_receiver_app/test/caf_bootstrap.js new file mode 100644 index 0000000000..721360e8a7 --- /dev/null +++ b/cast_receiver_app/test/caf_bootstrap.js @@ -0,0 +1,33 @@ +/* + * 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. + */ + +/** + * @fileoverview Declares constants which are provided by the CAF externs and + * are not included in uncompiled unit tests. + */ +cast = { + framework: { + system: { + EventType: { + SENDER_CONNECTED: 'sender_connected', + SENDER_DISCONNECTED: 'sender_disconnected', + }, + DisconnectReason: { + REQUESTED_BY_SENDER: 'requested_by_sender', + }, + }, + }, +}; diff --git a/cast_receiver_app/test/configuration_factory_test.js b/cast_receiver_app/test/configuration_factory_test.js new file mode 100644 index 0000000000..af9254c59e --- /dev/null +++ b/cast_receiver_app/test/configuration_factory_test.js @@ -0,0 +1,86 @@ +/* + * 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. + */ + +goog.module('exoplayer.cast.test.configurationfactory'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let configurationFactory; + +testSuite({ + setUp() { + configurationFactory = new ConfigurationFactory(); + }, + + /** Tests creating the most basic configuration. */ + testCreateBasicConfiguration() { + /** @type {!TrackSelectionParameters} */ + const selectionParameters = /** @type {!TrackSelectionParameters} */ ({ + preferredAudioLanguage: 'en', + preferredTextLanguage: 'it', + }); + const configuration = configurationFactory.createConfiguration( + util.queue.slice(0, 1), selectionParameters); + assertEquals('en', configuration.preferredAudioLanguage); + assertEquals('it', configuration.preferredTextLanguage); + // Assert empty drm configuration as default. + assertArrayEquals(['servers'], Object.keys(configuration.drm)); + assertArrayEquals([], Object.keys(configuration.drm.servers)); + }, + + /** Tests defaults for undefined audio and text languages. */ + testCreateBasicConfiguration_languagesUndefined() { + const configuration = configurationFactory.createConfiguration( + util.queue.slice(0, 1), /** @type {!TrackSelectionParameters} */ ({})); + assertEquals('', configuration.preferredAudioLanguage); + assertEquals('', configuration.preferredTextLanguage); + }, + + /** Tests creating a drm configuration */ + testCreateDrmConfiguration() { + /** @type {!MediaItem} */ + const mediaItem = util.queue[1]; + mediaItem.drmSchemes = [ + { + uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + licenseServer: { + uri: 'drm-uri0', + }, + }, + { + uuid: '9a04f079-9840-4286-ab92-e65be0885f95', + licenseServer: { + uri: 'drm-uri1', + }, + }, + { + uuid: 'unsupported-drm-uuid', + licenseServer: { + uri: 'drm-uri2', + }, + }, + ]; + const configuration = + configurationFactory.createConfiguration(mediaItem, {}); + assertEquals('drm-uri0', configuration.drm.servers['com.widevine.alpha']); + assertEquals( + 'drm-uri1', configuration.drm.servers['com.microsoft.playready']); + assertEquals(2, Object.entries(configuration.drm.servers).length); + } +}); diff --git a/cast_receiver_app/test/externs.js b/cast_receiver_app/test/externs.js new file mode 100644 index 0000000000..a90a367691 --- /dev/null +++ b/cast_receiver_app/test/externs.js @@ -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. + */ + +/** + * Externs for unit tests to avoid renaming of properties. + * + * These externs are only required when building with bazel because the + * closure_js_test compiles tests as well. + * + * @externs + */ + +/** @record */ +function ValidationObject() {} + +/** @type {*} */ +ValidationObject.prototype.field; + +/** @record */ +function Uuids() {} + +/** @type {!Array} */ +Uuids.prototype.uuids; diff --git a/cast_receiver_app/test/message_dispatcher_test.js b/cast_receiver_app/test/message_dispatcher_test.js new file mode 100644 index 0000000000..3e7daaf573 --- /dev/null +++ b/cast_receiver_app/test/message_dispatcher_test.js @@ -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. + * + * @fileoverview Unit tests for the message dispatcher. + */ + +goog.module('exoplayer.cast.test.messagedispatcher'); +goog.setTestOnly(); + +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); + +let contextMock; +let messageDispatcher; + +testSuite({ + setUp() { + mocks.setUp(); + contextMock = mocks.createCastReceiverContextFake(); + messageDispatcher = new MessageDispatcher( + 'urn:x-cast:com.google.exoplayer.cast', contextMock); + }, + + /** Test marshalling Infinity */ + testStringifyInfinity() { + const senderId = 'sender0'; + const name = 'Federico Vespucci'; + messageDispatcher.send(senderId, {name: name, duration: Infinity}); + + const msg = mocks.state().outputMessages[senderId][0]; + assertUndefined(msg.duration); + assertFalse(msg.hasOwnProperty('duration')); + assertEquals(name, msg.name); + assertTrue(msg.hasOwnProperty('name')); + } +}); diff --git a/cast_receiver_app/test/mocks.js b/cast_receiver_app/test/mocks.js new file mode 100644 index 0000000000..244ac72829 --- /dev/null +++ b/cast_receiver_app/test/mocks.js @@ -0,0 +1,277 @@ +/** + * 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. + * + * @fileoverview Mocks for testing cast components. + */ + +goog.module('exoplayer.cast.test.mocks'); +goog.setTestOnly(); + +const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); + +let mockState; +let manifest; + +/** + * Initializes the state of the mocks. Needs to be called in the setUp method of + * the unit test. + */ +const setUp = function() { + mockState = { + outputMessages: {}, + listeners: {}, + loadedUri: null, + preferredTextLanguage: '', + preferredAudioLanguage: '', + configuration: null, + responseFilter: null, + isSilent: false, + customMessageListener: undefined, + mediaElementState: { + removedAttributes: [], + }, + manifestState: { + isLive: false, + windowDuration: 20, + startTime: 0, + delay: 10, + }, + getManifest: () => manifest, + setManifest: (m) => { + manifest = m; + }, + shakaError: { + severity: /** CRITICAL */ 2, + code: /** not 7000 (LOAD_INTERUPTED) */ 3, + category: /** any */ 1, + }, + simulateLoad: simulateLoadSuccess, + /** @type {function(boolean)} */ + setShakaThrowsOnLoad: (doThrow) => { + mockState.simulateLoad = doThrow ? throwShakaError : simulateLoadSuccess; + }, + simulateUnload: simulateUnloadSuccess, + /** @type {function(boolean)} */ + setShakaThrowsOnUnload: (doThrow) => { + mockState.simulateUnload = + doThrow ? throwShakaError : simulateUnloadSuccess; + }, + onSenderConnected: undefined, + onSenderDisconnected: undefined, + }; + manifest = { + periods: [{startTime: mockState.manifestState.startTime}], + presentationTimeline: { + getDuration: () => mockState.manifestState.windowDuration, + isLive: () => mockState.manifestState.isLive, + getSegmentAvailabilityStart: () => 0, + getSegmentAvailabilityEnd: () => mockState.manifestState.windowDuration, + getSeekRangeStart: () => 0, + getSeekRangeEnd: () => mockState.manifestState.windowDuration - + mockState.manifestState.delay, + }, + }; +}; + +/** + * Simulates a successful `shakaPlayer.load` call. + * + * @param {string} uri The uri to load. + */ +const simulateLoadSuccess = (uri) => { + mockState.loadedUri = uri; + notifyListeners('streaming'); +}; + +/** Simulates a successful `shakaPlayer.unload` call. */ +const simulateUnloadSuccess = () => { + mockState.loadedUri = undefined; + notifyListeners('unloading'); +}; + +/** @throws {!ShakaError} Thrown in any case. */ +const throwShakaError = () => { + throw mockState.shakaError; +}; + + +/** + * Adds a fake event listener. + * + * @param {string} type The type of the listener. + * @param {function(!Object)} listener The callback listener. + */ +const addEventListener = function(type, listener) { + mockState.listeners[type] = mockState.listeners[type] || []; + mockState.listeners[type].push(listener); +}; + +/** + * Notifies the fake listeners of the given type. + * + * @param {string} type The type of the listener to notify. + */ +const notifyListeners = function(type) { + if (mockState.isSilent || !mockState.listeners[type]) { + return; + } + for (let i = 0; i < mockState.listeners[type].length; i++) { + mockState.listeners[type][i]({ + type: type + }); + } +}; + +/** + * Creates an observable for which listeners can be added. + * + * @return {!Object} An observable object. + */ +const createObservable = () => { + return { + addEventListener: (type, listener) => { + addEventListener(type, listener); + }, + }; +}; + +/** + * Creates a fake for the shaka player. + * + * @return {!shaka.Player} A shaka player mock object. + */ +const createShakaFake = () => { + const shakaFake = /** @type {!shaka.Player} */(createObservable()); + const mediaElement = createMediaElementFake(); + /** + * @return {!HTMLMediaElement} A media element. + */ + shakaFake.getMediaElement = () => mediaElement; + shakaFake.getAudioLanguages = () => []; + shakaFake.getVariantTracks = () => []; + shakaFake.configure = (configuration) => { + mockState.configuration = configuration; + return true; + }; + shakaFake.selectTextLanguage = (language) => { + mockState.preferredTextLanguage = language; + }; + shakaFake.selectAudioLanguage = (language) => { + mockState.preferredAudioLanguage = language; + }; + shakaFake.getManifest = () => manifest; + shakaFake.unload = async () => mockState.simulateUnload(); + shakaFake.load = async (uri) => mockState.simulateLoad(uri); + shakaFake.getNetworkingEngine = () => { + return /** @type {!NetworkingEngine} */ ({ + registerResponseFilter: (responseFilter) => { + mockState.responseFilter = responseFilter; + }, + unregisterResponseFilter: (responseFilter) => { + if (mockState.responseFilter !== responseFilter) { + throw new Error('unregistering invalid response filter'); + } else { + mockState.responseFilter = null; + } + }, + }); + }; + return shakaFake; +}; + +/** + * Creates a fake for a media element. + * + * @return {!HTMLMediaElement} A media element fake. + */ +const createMediaElementFake = () => { + const mediaElementFake = /** @type {!HTMLMediaElement} */(createObservable()); + mediaElementFake.load = () => { + // Do nothing. + }; + mediaElementFake.play = () => { + mediaElementFake.paused = false; + notifyListeners('playing'); + return Promise.resolve(); + }; + mediaElementFake.pause = () => { + mediaElementFake.paused = true; + notifyListeners('pause'); + }; + mediaElementFake.seekable = /** @type {!TimeRanges} */({ + length: 1, + start: (index) => mockState.manifestState.startTime, + end: (index) => mockState.manifestState.windowDuration, + }); + mediaElementFake.removeAttribute = (name) => { + mockState.mediaElementState.removedAttributes.push(name); + if (name === 'src') { + mockState.loadedUri = null; + } + }; + mediaElementFake.hasAttribute = (name) => { + return name === 'src' && !!mockState.loadedUri; + }; + mediaElementFake.buffered = /** @type {!TimeRanges} */ ({ + length: 0, + start: (index) => null, + end: (index) => null, + }); + mediaElementFake.paused = true; + return mediaElementFake; +}; + +/** + * Creates a cast receiver manager fake. + * + * @return {!Object} A cast receiver manager fake. + */ +const createCastReceiverContextFake = () => { + return { + addCustomMessageListener: (namespace, listener) => { + mockState.customMessageListener = listener; + }, + sendCustomMessage: (namespace, senderId, message) => { + mockState.outputMessages[senderId] = + mockState.outputMessages[senderId] || []; + mockState.outputMessages[senderId].push(message); + }, + addEventListener: (eventName, listener) => { + switch (eventName) { + case 'sender_connected': + mockState.onSenderConnected = listener; + break; + case 'sender_disconnected': + mockState.onSenderDisconnected = listener; + break; + } + }, + getSenders: () => [{id: 'sender0'}], + start: () => {}, + }; +}; + +/** + * Returns the state of the mocks. + * + * @return {?Object} + */ +const state = () => mockState; + +exports.createCastReceiverContextFake = createCastReceiverContextFake; +exports.createShakaFake = createShakaFake; +exports.notifyListeners = notifyListeners; +exports.setUp = setUp; +exports.state = state; diff --git a/cast_receiver_app/test/playback_info_view_test.js b/cast_receiver_app/test/playback_info_view_test.js new file mode 100644 index 0000000000..87cefe1884 --- /dev/null +++ b/cast_receiver_app/test/playback_info_view_test.js @@ -0,0 +1,242 @@ +/** + * 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. + * + * @fileoverview Unit tests for the playback info view. + */ + +goog.module('exoplayer.cast.test.PlaybackInfoView'); +goog.setTestOnly(); + +const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); +const Player = goog.require('exoplayer.cast.Player'); +const testSuite = goog.require('goog.testing.testSuite'); + +/** The state of the player mock */ +let mockState; + +/** + * Initializes the state of the mock. Needs to be called in the setUp method of + * the unit test. + */ +const setUpMockState = function() { + mockState = { + playWhenReady: false, + currentPositionMs: 1000, + durationMs: 10 * 1000, + playbackState: 'READY', + discontinuityReason: undefined, + listeners: [], + currentMediaItem: { + mimeType: 'video/*', + }, + }; +}; + +/** Notifies registered listeners with the current player state. */ +const notifyListeners = function() { + if (!mockState) { + console.warn( + 'mock state not initialized. Did you call setUp ' + + 'when setting up the test case?'); + } + mockState.listeners.forEach((listener) => { + listener({ + playWhenReady: mockState.playWhenReady, + playbackState: mockState.playbackState, + playbackPosition: { + currentPositionMs: mockState.currentPositionMs, + discontinuityReason: mockState.discontinuityReason, + }, + }); + }); +}; + +/** + * Creates a sufficient mock of the Player. + * + * @return {!Player} + */ +const createPlayerMock = function() { + return /** @type {!Player} */ ({ + addPlayerListener: (listener) => { + mockState.listeners.push(listener); + }, + getPlayWhenReady: () => mockState.playWhenReady, + getPlaybackState: () => mockState.playbackState, + getCurrentPositionMs: () => mockState.currentPositionMs, + getDurationMs: () => mockState.durationMs, + getCurrentMediaItem: () => mockState.currentMediaItem, + }); +}; + +/** Inserts the DOM structure the playback info view needs. */ +const insertComponentDom = function() { + const container = appendChild(document.body, 'div', 'container-id'); + appendChild(container, 'div', 'exo_elapsed_time'); + appendChild(container, 'div', 'exo_elapsed_time_label'); + appendChild(container, 'div', 'exo_duration_label'); +}; + +/** + * Creates and appends a child to the parent element. + * + * @param {!Element} parent The parent element. + * @param {string} tagName The tag name of the child element. + * @param {string} id The id of the child element. + * @return {!Element} The appended child element. + */ +const appendChild = function(parent, tagName, id) { + const child = document.createElement(tagName); + child.id = id; + parent.appendChild(child); + return child; +}; + +/** Removes the inserted elements from the DOM again. */ +const removeComponentDom = function() { + const container = document.getElementById('container-id'); + if (container) { + container.parentNode.removeChild(container); + } +}; + +let playbackInfoView; + +testSuite({ + setUp() { + insertComponentDom(); + setUpMockState(); + playbackInfoView = new PlaybackInfoView( + createPlayerMock(), /** containerId= */ 'container-id'); + playbackInfoView.setShowTimeoutMs(1); + }, + + tearDown() { + removeComponentDom(); + }, + + /** Tests setting the show timeout. */ + testSetShowTimeout() { + assertEquals(1, playbackInfoView.showTimeoutMs_); + playbackInfoView.setShowTimeoutMs(10); + assertEquals(10, playbackInfoView.showTimeoutMs_); + }, + + /** Tests rendering the duration to the DOM. */ + testRenderDuration() { + const el = document.getElementById('exo_duration_label'); + assertEquals('00:10', el.firstChild.firstChild.nodeValue); + mockState.durationMs = 35 * 1000; + notifyListeners(); + assertEquals('00:35', el.firstChild.firstChild.nodeValue); + + mockState.durationMs = + (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); + notifyListeners(); + assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); + + mockState.durationMs = -1000; + notifyListeners(); + assertNull(el.nodeValue); + }, + + /** Tests rendering the playback position to the DOM. */ + testRenderPlaybackPosition() { + const el = document.getElementById('exo_elapsed_time_label'); + assertEquals('00:01', el.firstChild.firstChild.nodeValue); + mockState.currentPositionMs = 2000; + notifyListeners(); + assertEquals('00:02', el.firstChild.firstChild.nodeValue); + + mockState.currentPositionMs = + (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); + notifyListeners(); + assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); + + mockState.currentPositionMs = -1000; + notifyListeners(); + assertNull(el.nodeValue); + + mockState.currentPositionMs = 0; + notifyListeners(); + assertEquals('00:00', el.firstChild.firstChild.nodeValue); + }, + + /** Tests rendering the timebar width reflects position and duration. */ + testRenderTimebar() { + const el = document.getElementById('exo_elapsed_time'); + assertEquals('10%', el.style.width); + + mockState.currentPositionMs = 0; + notifyListeners(); + assertEquals('0px', el.style.width); + + mockState.currentPositionMs = 5 * 1000; + notifyListeners(); + assertEquals('50%', el.style.width); + + mockState.currentPositionMs = mockState.durationMs * 2; + notifyListeners(); + assertEquals('100%', el.style.width); + + mockState.currentPositionMs = -1; + notifyListeners(); + assertEquals('0px', el.style.width); + }, + + /** Tests whether the update timeout is set and removed. */ + testUpdateTimeout_setAndRemoved() { + assertFalse(playbackInfoView.updateTimeout_.isOngoing()); + + mockState.playWhenReady = true; + notifyListeners(); + assertTrue(playbackInfoView.updateTimeout_.isOngoing()); + + mockState.playWhenReady = false; + notifyListeners(); + assertFalse(playbackInfoView.updateTimeout_.isOngoing()); + }, + + /** Tests whether the show timeout is set when playback starts. */ + testHideTimeout_setAndRemoved() { + assertFalse(playbackInfoView.hideTimeout_.isOngoing()); + + mockState.playWhenReady = true; + notifyListeners(); + assertNotUndefined(playbackInfoView.hideTimeout_); + assertTrue(playbackInfoView.hideTimeout_.isOngoing()); + + mockState.playWhenReady = false; + notifyListeners(); + assertFalse(playbackInfoView.hideTimeout_.isOngoing()); + }, + + /** Test whether the view switches to always on for audio media. */ + testAlwaysOnForAudio() { + playbackInfoView.setShowTimeoutMs(50); + assertEquals(50, playbackInfoView.showTimeoutMs_); + // The player transitions from video to audio stream. + mockState.discontinuityReason = 'PERIOD_TRANSITION'; + mockState.currentMediaItem.mimeType = 'audio/*'; + notifyListeners(); + assertEquals(0, playbackInfoView.showTimeoutMs_); + + mockState.discontinuityReason = 'PERIOD_TRANSITION'; + mockState.currentMediaItem.mimeType = 'video/*'; + notifyListeners(); + assertEquals(50, playbackInfoView.showTimeoutMs_); + }, + +}); diff --git a/cast_receiver_app/test/player_test.js b/cast_receiver_app/test/player_test.js new file mode 100644 index 0000000000..96dfbf8614 --- /dev/null +++ b/cast_receiver_app/test/player_test.js @@ -0,0 +1,470 @@ +/** + * 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. + * + * @fileoverview Unit tests for playback methods. + */ + +goog.module('exoplayer.cast.test'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const Player = goog.require('exoplayer.cast.Player'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let player; +let shakaFake; + +testSuite({ + setUp() { + mocks.setUp(); + shakaFake = mocks.createShakaFake(); + player = new Player(shakaFake, new ConfigurationFactory()); + }, + + /** Tests the player initialisation */ + testPlayerInitialisation() { + mocks.state().isSilent = true; + const states = []; + let stateCounter = 0; + let currentState; + player.addPlayerListener((playerState) => { + states.push(playerState); + }); + + // Dump the initial state manually. + player.invalidate(); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(0, currentState.mediaQueue.length); + assertEquals(0, currentState.windowIndex); + assertNull(currentState.playbackPosition); + + // Seek with uuid to prepare with later + const uuid = 'uuid1'; + player.seekToUuid(uuid, 30 * 1000); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(30 * 1000, player.getCurrentPositionMs()); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(-1, player.windowIndex_); + assertEquals(1, currentState.playbackPosition.periodId); + assertEquals(uuid, currentState.playbackPosition.uuid); + assertEquals(uuid, player.uuidToPrepare_); + + // Add a DASH media item. + player.addQueueItems(0, util.queue.slice(0, 2)); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals('IDLE', currentState.playbackState); + assertNotNull(currentState.playbackPosition); + util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); + + // Prepare. + player.prepare(); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(2, currentState.mediaQueue.length); + assertEquals('BUFFERING', currentState.playbackState); + assertEquals( + Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid]); + assertNull(player.uuidToPrepare_); + + // The video element starts waiting. + mocks.state().isSilent = false; + mocks.notifyListeners('waiting'); + // Nothing happens, masked buffering state after preparing. + assertEquals(stateCounter, states.length); + + // The manifest arrives. + mocks.notifyListeners('streaming'); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(2, currentState.mediaQueue.length); + assertEquals('BUFFERING', currentState.playbackState); + assertEquals(uuid, currentState.playbackPosition.uuid); + assertEquals(0, currentState.playbackPosition.periodId); + assertEquals(30 * 1000, currentState.playbackPosition.positionMs); + // The dummy media item info has been replaced by the real one. + assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); + assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); + assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); + + // Tracks have initially changed. + mocks.notifyListeners('trackschanged'); + // Nothing happens because the media item info remains the same. + assertEquals(stateCounter, states.length); + + // The video element reports the first frame rendered. + mocks.notifyListeners('loadeddata'); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + assertEquals(2, currentState.mediaQueue.length); + assertEquals('READY', currentState.playbackState); + assertEquals(uuid, currentState.playbackPosition.uuid); + assertEquals(0, currentState.playbackPosition.periodId); + assertEquals(30 * 1000, currentState.playbackPosition.positionMs); + + // Playback starts. + mocks.notifyListeners('playing'); + // Nothing happens; we are ready already. + assertEquals(stateCounter, states.length); + + // Add another queue item. + player.addQueueItems(1, util.queue.slice(3, 4)); + stateCounter++; + assertEquals(stateCounter, states.length); + mocks.state().isSilent = true; + // Seek to the next queue item. + player.seekToWindow(1, 0); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + const uuid1 = currentState.mediaQueue[1].uuid; + assertEquals( + Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid1]); + util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); + + // The video element starts waiting. + mocks.state().isSilent = false; + mocks.notifyListeners('waiting'); + // Nothing happens, masked buffering state after preparing. + assertEquals(stateCounter, states.length); + + // The manifest arrives. + mocks.notifyListeners('streaming'); + stateCounter++; + assertEquals(stateCounter, states.length); + currentState = states[stateCounter - 1]; + // The dummy media item info has been replaced by the real one. + assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); + assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); + assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); + assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); + }, + + /** Tests next and previous window when not yet prepared. */ + testNextPreviousWindow_notPrepared() { + assertEquals(-1, player.getNextWindowIndex()); + assertEquals(-1, player.getPreviousWindowIndex()); + player.addQueueItems(0, util.queue.slice(0, 2)); + assertEquals(-1, player.getNextWindowIndex()); + assertEquals(-1, player.getPreviousWindowIndex()); + }, + + /** Tests setting play when ready. */ + testPlayWhenReady() { + player.addQueueItems(0, util.queue.slice(0, 3)); + let playWhenReady = false; + player.addPlayerListener((state) => { + playWhenReady = state.playWhenReady; + }); + + assertEquals(false, player.getPlayWhenReady()); + assertEquals(false, playWhenReady); + + player.setPlayWhenReady(true); + assertEquals(true, player.getPlayWhenReady()); + assertEquals(true, playWhenReady); + + player.setPlayWhenReady(false); + assertEquals(false, player.getPlayWhenReady()); + assertEquals(false, playWhenReady); + }, + + /** Tests seeking to another position in the actual window. */ + async testSeek_inWindow() { + player.addQueueItems(0, util.queue.slice(0, 3)); + await player.seekToWindow(0, 1000); + + assertEquals(1, shakaFake.getMediaElement().currentTime); + assertEquals(1000, player.getCurrentPositionMs()); + assertEquals(0, player.getCurrentWindowIndex()); + }, + + /** Tests seeking to another window. */ + async testSeek_nextWindow() { + player.addQueueItems(0, util.queue.slice(0, 3)); + await player.prepare(); + assertEquals(util.queue[0].media.uri, shakaFake.getMediaElement().src); + assertEquals(-1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.seekToWindow(1, 2000); + assertEquals(0, player.getPreviousWindowIndex()); + assertEquals(2, player.getNextWindowIndex()); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(util.queue[1].media.uri, mocks.state().loadedUri); + }, + + /** Tests the repeat mode 'none' */ + testRepeatMode_none() { + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + assertEquals(Player.RepeatMode.OFF, player.getRepeatMode()); + assertEquals(-1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.seekToWindow(2, 0); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(-1, player.getNextWindowIndex()); + }, + + /** Tests the repeat mode 'all'. */ + testRepeatMode_all() { + let repeatMode; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.addPlayerListener((state) => { + repeatMode = state.repeatMode; + }); + player.setRepeatMode(Player.RepeatMode.ALL); + assertEquals(Player.RepeatMode.ALL, repeatMode); + + player.seekToWindow(0,0); + assertEquals(2, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.seekToWindow(2, 0); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(0, player.getNextWindowIndex()); + }, + + /** + * Tests navigation within the queue when repeat mode and shuffle mode is on. + */ + testRepeatMode_all_inShuffleMode() { + const initialOrder = [2, 1, 0]; + let shuffleOrder; + let windowIndex; + player.addQueueItems(0, util.queue.slice(0, 3), initialOrder); + player.prepare(); + player.addPlayerListener((state) => { + shuffleOrder = state.shuffleOrder; + windowIndex = state.windowIndex; + }); + player.setRepeatMode(Player.RepeatMode.ALL); + player.setShuffleModeEnabled(true); + assertEquals(windowIndex, player.shuffleOrder_[player.shuffleIndex_]); + assertArrayEquals(initialOrder, shuffleOrder); + + player.seekToWindow(shuffleOrder[2], 0); + assertEquals(shuffleOrder[2], windowIndex); + assertEquals(shuffleOrder[0], player.getNextWindowIndex()); + assertEquals(shuffleOrder[1], player.getPreviousWindowIndex()); + + player.seekToWindow(shuffleOrder[0], 0); + assertEquals(shuffleOrder[0], windowIndex); + }, + + /** Tests the repeat mode 'one' */ + testRepeatMode_one() { + let repeatMode; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.addPlayerListener((state) => { + repeatMode = state.repeatMode; + }); + player.setRepeatMode(Player.RepeatMode.ONE); + assertEquals(Player.RepeatMode.ONE, repeatMode); + assertEquals(0, player.getPreviousWindowIndex()); + assertEquals(0, player.getNextWindowIndex()); + + player.seekToWindow(1, 0); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + + player.setShuffleModeEnabled(true); + assertEquals(1, player.getPreviousWindowIndex()); + assertEquals(1, player.getNextWindowIndex()); + }, + + /** Tests building a media item info from the manifest. */ + testBuildMediaItemInfo_fromManifest() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + player.seekToWindow(1, 0); + player.prepare(); + assertUndefined(mediaItemInfos['uuid0']); + const mediaItemInfo = mediaItemInfos['uuid1']; + assertNotUndefined(mediaItemInfo); + assertFalse(mediaItemInfo.isDynamic); + assertTrue(mediaItemInfo.isSeekable); + assertEquals(0, mediaItemInfo.defaultStartPositionUs); + assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); + assertEquals(1, mediaItemInfo.periods.length); + assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); + }, + + /** Tests building a media item info with multiple periods. */ + testBuildMediaItemInfo_fromManifest_multiPeriod() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + // Setting manifest properties to emulate a multiperiod stream manifest. + mocks.state().getManifest().periods.push({startTime: 20}); + mocks.state().manifestState.windowDuration = 50; + player.seekToWindow(1, 0); + player.prepare(); + + const mediaItemInfo = mediaItemInfos['uuid1']; + assertNotUndefined(mediaItemInfo); + assertFalse(mediaItemInfo.isDynamic); + assertTrue(mediaItemInfo.isSeekable); + assertEquals(0, mediaItemInfo.defaultStartPositionUs); + assertEquals(50 * 1000 * 1000, mediaItemInfo.windowDurationUs); + assertEquals(2, mediaItemInfo.periods.length); + assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); + assertEquals(30 * 1000 * 1000, mediaItemInfo.periods[1].durationUs); + }, + + /** Tests building a media item info from a live manifest. */ + testBuildMediaItemInfo_fromManifest_live() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + // Setting manifest properties to emulate a live stream manifest. + mocks.state().manifestState.isLive = true; + mocks.state().manifestState.windowDuration = 30; + mocks.state().manifestState.delay = 10; + mocks.state().getManifest().periods.push({startTime: 20}); + player.seekToWindow(1, 0); + player.prepare(); + + const mediaItemInfo = mediaItemInfos['uuid1']; + assertNotUndefined(mediaItemInfo); + assertTrue(mediaItemInfo.isDynamic); + assertTrue(mediaItemInfo.isSeekable); + assertEquals(20 * 1000 * 1000, mediaItemInfo.defaultStartPositionUs); + assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); + assertEquals(2, mediaItemInfo.periods.length); + assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); + assertEquals(Infinity, mediaItemInfo.periods[1].durationUs); + }, + + /** Tests whether the shaka request filter is set for life streams. */ + testRequestFilterIsSetAndRemovedForLive() { + player.addQueueItems(0, util.queue.slice(0, 3)); + + // Set manifest properties to emulate a live stream manifest. + mocks.state().manifestState.isLive = true; + mocks.state().manifestState.windowDuration = 30; + mocks.state().manifestState.delay = 10; + mocks.state().getManifest().periods.push({startTime: 20}); + + assertNull(mocks.state().responseFilter); + assertFalse(player.isManifestFilterRegistered_); + player.seekToWindow(1, 0); + player.prepare(); + assertNotNull(mocks.state().responseFilter); + assertTrue(player.isManifestFilterRegistered_); + + // Set manifest properties to emulate a non-live stream */ + mocks.state().manifestState.isLive = false; + mocks.state().manifestState.windowDuration = 20; + mocks.state().manifestState.delay = 0; + mocks.state().getManifest().periods.push({startTime: 20}); + + player.seekToWindow(0, 0); + assertNull(mocks.state().responseFilter); + assertFalse(player.isManifestFilterRegistered_); + }, + + /** Tests whether the media info is removed when queue item is removed. */ + testRemoveMediaItemInfo() { + let mediaItemInfos = null; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + mediaItemInfos = state.mediaItemsInfo; + }); + player.seekToWindow(1, 0); + player.prepare(); + assertNotUndefined(mediaItemInfos['uuid1']); + player.removeQueueItems(['uuid1']); + assertUndefined(mediaItemInfos['uuid1']); + }, + + /** Tests shuffling. */ + testSetShuffeModeEnabled() { + let shuffleModeEnabled = false; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.addPlayerListener((state) => { + shuffleModeEnabled = state.shuffleModeEnabled; + }); + player.setShuffleModeEnabled(true); + assertTrue(shuffleModeEnabled); + + player.setShuffleModeEnabled(false); + assertFalse(shuffleModeEnabled); + }, + + /** Tests setting a new playback order. */ + async testSetShuffleOrder() { + const defaultOrder = [0, 1, 2]; + let shuffleOrder; + player.addPlayerListener((state) => { + shuffleOrder = state.shuffleOrder; + }); + await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); + assertArrayEquals(defaultOrder, shuffleOrder); + + player.setShuffleOrder_([2, 1, 0]); + assertArrayEquals([2, 1, 0], player.shuffleOrder_); + }, + + /** Tests setting a new playback order with incorrect length. */ + async testSetShuffleOrder_incorrectLength() { + const defaultOrder = [0, 1, 2]; + let shuffleOrder; + player.addPlayerListener((state) => { + shuffleOrder = state.shuffleOrder; + }); + await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); + assertArrayEquals(defaultOrder, shuffleOrder); + + shuffleOrder = undefined; + player.setShuffleOrder_([2, 1]); + assertUndefined(shuffleOrder); + }, + + /** Tests falling into ENDED when prepared with empty queue. */ + testPrepare_withEmptyQueue() { + player.seekToUuid('uuid1000', 1000); + assertEquals('uuid1000', player.uuidToPrepare_); + player.prepare(); + assertEquals('ENDED', player.getPlaybackState()); + assertNull(player.uuidToPrepare_); + player.seekToUuid('uuid1000', 1000); + assertNull(player.uuidToPrepare_); + }, +}); diff --git a/cast_receiver_app/test/queue_test.js b/cast_receiver_app/test/queue_test.js new file mode 100644 index 0000000000..b46361fb2e --- /dev/null +++ b/cast_receiver_app/test/queue_test.js @@ -0,0 +1,166 @@ +/** + * 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. + * + * @fileoverview Unit tests for queue manipulations. + */ + +goog.module('exoplayer.cast.test.queue'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const Player = goog.require('exoplayer.cast.Player'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let player; + +testSuite({ + setUp() { + mocks.setUp(); + player = new Player(mocks.createShakaFake(), new ConfigurationFactory()); + }, + + /** Tests adding queue items. */ + testAddQueueItem() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + assertEquals(0, queue.length); + player.addQueueItems(0, util.queue.slice(0, 3)); + assertEquals(util.queue[0].media.uri, queue[0].media.uri); + assertEquals(util.queue[1].media.uri, queue[1].media.uri); + assertEquals(util.queue[2].media.uri, queue[2].media.uri); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests that duplicate queue items are ignored. */ + testAddDuplicateQueueItem() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + assertEquals(0, queue.length); + // Insert three items. + player.addQueueItems(0, util.queue.slice(0, 3)); + // Insert two of which the first is a duplicate. + player.addQueueItems(1, util.queue.slice(2, 4)); + assertEquals(4, queue.length); + assertArrayEquals( + ['uuid0', 'uuid3', 'uuid1', 'uuid2'], queue.slice().map((i) => i.uuid)); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving queue items. */ + testMoveQueueItem() { + const shuffleOrder = [0, 2, 1]; + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.moveQueueItem('uuid0', 1, shuffleOrder); + assertEquals(util.queue[1].media.uri, queue[0].media.uri); + assertEquals(util.queue[0].media.uri, queue[1].media.uri); + assertEquals(util.queue[2].media.uri, queue[2].media.uri); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + + queue = undefined; + // invalid to index + player.moveQueueItem('uuid0', 11, [0, 1, 2]); + assertTrue(typeof queue === 'undefined'); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + // negative to index + player.moveQueueItem('uuid0', -11, shuffleOrder); + assertTrue(typeof queue === 'undefined'); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + // unknown uuid + player.moveQueueItem('unknown', 1, shuffleOrder); + assertTrue(typeof queue === 'undefined'); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + }, + + /** Tests removing queue items. */ + testRemoveQueueItems() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.prepare(); + player.seekToWindow(1, 0); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Remove the first item. + player.removeQueueItems(['uuid0']); + assertEquals(2, queue.length); + assertEquals(util.queue[1].media.uri, queue[0].media.uri); + assertEquals(util.queue[2].media.uri, queue[1].media.uri); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([1,0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Calling stop without reseting preserves the queue. + player.stop(false); + assertEquals('uuid1', player.uuidToPrepare_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Remove the item at the end of the queue. + player.removeQueueItems(['uuid2']); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + + // Remove the last remaining item in the queue. + player.removeQueueItems(['uuid1']); + assertEquals(0, queue.length); + assertEquals('IDLE', player.getPlaybackState()); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([], player.shuffleOrder_); + assertNull(player.uuidToPrepare_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); + }, + + /** Tests removing multiple unordered queue items at once. */ + testRemoveQueueItems_multiple() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 6), []); + player.prepare(); + + assertEquals(6, queue.length); + player.removeQueueItems(['uuid1', 'uuid5', 'uuid3']); + assertArrayEquals(['uuid0', 'uuid2', 'uuid4'], queue.map((i) => i.uuid)); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests whether stopping with reset=true resets queue and uuidToIndexMap */ + testStop_resetTrue() { + let queue = []; + player.addPlayerListener((state) => { + queue = state.mediaQueue; + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.prepare(); + player.stop(true); + assertEquals(0, player.queue_.length); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, +}); diff --git a/cast_receiver_app/test/receiver_test.js b/cast_receiver_app/test/receiver_test.js new file mode 100644 index 0000000000..303a1caf64 --- /dev/null +++ b/cast_receiver_app/test/receiver_test.js @@ -0,0 +1,1027 @@ +/** + * 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. + * + * @fileoverview Unit tests for receiver. + */ + +goog.module('exoplayer.cast.test.receiver'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); +const Player = goog.require('exoplayer.cast.Player'); +const Receiver = goog.require('exoplayer.cast.Receiver'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +/** @type {?Player|undefined} */ +let player; +/** @type {!Array} */ +let queue = []; +let shakaFake; +let castContextMock; + +/** + * Sends a message to the receiver under test. + * + * @param {!Object} message The message to send as json. + */ +const sendMessage = function(message) { + mocks.state().customMessageListener({ + data: message, + senderId: 'sender0', + }); +}; + +/** + * Creates a valid media item with the suffix appended to each field. + * + * @param {string} suffix The suffix to append to the fields value. + * @return {!Object} The media item. + */ +const createMediaItem = function(suffix) { + return { + uuid: 'uuid' + suffix, + media: {uri: 'uri' + suffix}, + mimeType: 'application/dash+xml', + }; +}; + +let messageSequence = 0; + +/** + * Creates a message in the format sent bey the sender app. + * + * @param {string} method The name of the method. + * @param {?Object} args The arguments. + * @return {!Object} The message. + */ +const createMessage = function (method, args) { + return { + method: method, + args: args, + sequenceNumber: ++messageSequence, + }; +}; + +/** + * Asserts the `playerState` is in the same state as just after creation of the + * player. + * + * @param {!PlayerState} playerState The player state to assert. + * @param {string} playbackState The expected playback state. + */ +const assertInitialState = function(playerState, playbackState) { + assertEquals(playbackState, playerState.playbackState); + // Assert the state is in initial state. + assertArrayEquals([], queue); + assertEquals(0, playerState.windowCount); + assertEquals(0, playerState.windowIndex); + assertUndefined(playerState.playbackError); + assertNull(playerState.playbackPosition); + // Assert player properties. + assertEquals(0, player.getDurationMs()); + assertArrayEquals([], Object.entries(player.mediaItemInfoMap_)); + assertEquals(0, player.windowPeriodIndex_); + assertEquals(999, player.playbackType_); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); +}; + + +testSuite({ + setUp() { + mocks.setUp(); + shakaFake = mocks.createShakaFake(); + castContextMock = mocks.createCastReceiverContextFake(); + player = new Player(shakaFake, new ConfigurationFactory()); + player.addPlayerListener((playerState) => { + queue = playerState.mediaQueue; + }); + const messageDispatcher = new MessageDispatcher( + 'urn:x-cast:com.google.exoplayer.cast', castContextMock); + new Receiver(player, castContextMock, messageDispatcher); + }, + + tearDown() { + queue = []; + }, + + /** Tests whether a status was sent to the sender on connect. */ + testNotifyClientConnected() { + assertUndefined(mocks.state().outputMessages['sender0']); + + sendMessage(createMessage('player.onClientConnected', {})); + const message = mocks.state().outputMessages['sender0'][0]; + assertEquals(messageSequence, message.sequenceNumber); + }, + + /** + * Tests whether a custom message listener has been registered after + * construction. + */ + testCustomMessageListener() { + assertTrue(goog.isFunction(mocks.state().customMessageListener)); + }, + + /** Tests set playWhenReady. */ + testSetPlayWhenReady() { + let playWhenReady; + player.addPlayerListener((playerState) => { + playWhenReady = playerState.playWhenReady; + }); + + sendMessage(createMessage( + 'player.setPlayWhenReady', + { playWhenReady: true } + )); + assertTrue(playWhenReady); + sendMessage(createMessage( + 'player.setPlayWhenReady', + { playWhenReady: false } + )); + assertFalse(playWhenReady); + }, + + /** Tests setting repeat modes. */ + testSetRepeatMode() { + let repeatMode; + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.addPlayerListener((playerState) => { + repeatMode = playerState.repeatMode; + }); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: Player.RepeatMode.ONE } + )); + assertEquals(Player.RepeatMode.ONE, repeatMode); + assertEquals(0, player.getNextWindowIndex()); + assertEquals(0, player.getPreviousWindowIndex()); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: Player.RepeatMode.ALL } + )); + assertEquals(Player.RepeatMode.ALL, repeatMode); + assertEquals(1, player.getNextWindowIndex()); + assertEquals(2, player.getPreviousWindowIndex()); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: Player.RepeatMode.OFF } + )); + assertEquals(Player.RepeatMode.OFF, repeatMode); + assertEquals(1, player.getNextWindowIndex()); + assertTrue(player.getPreviousWindowIndex() < 0); + }, + + /** Tests setting an invalid repeat mode value. */ + testSetRepeatMode_invalid_noStateChange() { + let repeatMode; + player.addPlayerListener((playerState) => { + repeatMode = playerState.repeatMode; + }); + + sendMessage(createMessage( + 'player.setRepeatMode', + { repeatMode: "UNKNOWN" } + )); + assertEquals(Player.RepeatMode.OFF, player.repeatMode_); + assertUndefined(repeatMode); + player.invalidate(); + assertEquals(Player.RepeatMode.OFF, repeatMode); + }, + + /** Tests enabling and disabling shuffle mode. */ + testSetShuffleModeEnabled() { + const enableMessage = createMessage('player.setShuffleModeEnabled', { + shuffleModeEnabled: true, + }); + const disableMessage = createMessage('player.setShuffleModeEnabled', { + shuffleModeEnabled: false, + }); + let shuffleModeEnabled; + player.addPlayerListener((state) => { + shuffleModeEnabled = state.shuffleModeEnabled; + }); + assertFalse(player.shuffleModeEnabled_); + sendMessage(enableMessage); + assertTrue(shuffleModeEnabled); + sendMessage(disableMessage); + assertFalse(shuffleModeEnabled); + }, + + /** Tests adding a single media item to the queue. */ + testAddMediaItem_single() { + const suffix = '0'; + const jsonMessage = createMessage('player.addItems', { + index: 0, + items: [ + createMediaItem(suffix), + ], + shuffleOrder: [0], + }); + + sendMessage(jsonMessage); + assertEquals(1, queue.length); + assertEquals('uuid0', queue[0].uuid); + assertEquals('uri0', queue[0].media.uri); + assertArrayEquals([0], player.shuffleOrder_); + }, + + /** Tests adding multiple media items to the queue. */ + testAddMediaItem_multiple() { + const shuffleOrder = [0, 2, 1]; + const jsonMessage = createMessage('player.addItems', { + index: 0, + items: [ + createMediaItem('0'), + createMediaItem('1'), + createMediaItem('2'), + ], + shuffleOrder: shuffleOrder, + }); + + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + }, + + /** Tests adding a media item to end of the queue by omitting the index. */ + testAddMediaItem_noindex_addstoend() { + const shuffleOrder = [1, 3, 2, 0]; + const jsonMessage = createMessage('player.addItems', { + items: [createMediaItem('99')], + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + let queue = []; + player.addPlayerListener((playerState) => { + queue = playerState.mediaQueue; + }); + sendMessage(jsonMessage); + assertEquals(4, queue.length); + assertEquals('uuid99', queue[3].uuid); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + }, + + /** Tests adding items with a shuffle order of invalid length. */ + testAddMediaItems_invalidShuffleOrderLength() { + const shuffleOrder = [1, 3, 2]; + const jsonMessage = createMessage('player.addItems', { + items: [createMediaItem('99')], + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + let queue = []; + player.addPlayerListener((playerState) => { + queue = playerState.mediaQueue; + }); + sendMessage(jsonMessage); + assertEquals(4, queue.length); + assertEquals('uuid99', queue[3].uuid); + assertEquals(4, player.shuffleOrder_.length); + }, + + /** Tests inserting a media item to the queue. */ + testAddMediaItem_insert() { + const index = 1; + const shuffleOrder = [1, 0, 3, 2, 4]; + const firstInsertionMessage = createMessage('player.addItems', { + index, + items: [ + createMediaItem('99'), + createMediaItem('100'), + ], + shuffleOrder, + }); + const prepareMessage = createMessage('player.prepare', {}); + const secondInsertionMessage = createMessage('player.addItems', { + index, + items: [ + createMediaItem('199'), + createMediaItem('1100'), + ], + shuffleOrder, + }); + // fill with three items + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToUuid('uuid99', 0); + + sendMessage(firstInsertionMessage); + // The window index does not change when IDLE. + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(5, queue.length); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + + // Prepare sets the index by the uuid to which we seeked. + sendMessage(prepareMessage); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + // Add two items at the current window index. + sendMessage(secondInsertionMessage); + // Current window index is adjusted. + assertEquals(3, player.getCurrentWindowIndex()); + assertEquals(7, queue.length); + assertEquals('uuid199', queue[index].uuid); + assertEquals(7, player.shuffleOrder_.length); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests adding a media item with an index larger than the queue size. */ + testAddMediaItem_indexLargerThanQueueSize_addsToEnd() { + const index = 4; + const jsonMessage = createMessage('player.addItems', { + index: index, + items: [ + createMediaItem('99'), + createMediaItem('100'), + ], + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid99', 'uuid100'], + queue.map((x) => x.uuid)); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing an item from the queue. */ + testRemoveMediaItem() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + + sendMessage(jsonMessage); + assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); + assertArrayEquals([0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing the currently playing item from the queue. */ + async testRemoveMediaItem_currentItem() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToWindow(1, 0); + player.prepare(); + + await sendMessage(jsonMessage); + assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); + assertEquals(0, player.getCurrentWindowIndex()); + assertEquals(util.queue[2].media.uri, shakaFake.getMediaElement().src); + assertArrayEquals([0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing items which affect the current window index. */ + async testRemoveMediaItem_affectsWindowIndex() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); + const currentUri = util.queue[4].media.uri; + player.addQueueItems(0, util.queue.slice(0, 6), [3, 2, 1, 4, 0, 5]); + player.prepare(); + await player.seekToWindow(4, 2000); + assertEquals(currentUri, shakaFake.getMediaElement().src); + + sendMessage(jsonMessage); + assertEquals(4, queue.length); + assertEquals('uuid4', queue[player.getCurrentWindowIndex()].uuid); + assertEquals(2, player.getCurrentWindowIndex()); + assertEquals(currentUri, shakaFake.getMediaElement().src); + assertArrayEquals([1, 0, 2, 3], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing the last item of the queue. */ + testRemoveMediaItem_firstItem_windowIndexIsCorrect() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid0']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToWindow(1, 0); + + sendMessage(jsonMessage); + assertArrayEquals(['uuid1', 'uuid2'], queue.map((x) => x.uuid)); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([1, 0], player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing the last item of the queue. */ + testRemoveMediaItem_lastItem_windowIndexIsCorrect() { + const jsonMessage = + createMessage('player.removeItems', {uuids: ['uuid2']}); + player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); + player.seekToWindow(2, 0); + player.prepare(); + + mocks.state().isSilent = true; + const states = []; + player.addPlayerListener((playerState) => { + states.push(playerState); + }); + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1'], queue.map((x) => x.uuid)); + assertEquals(1, player.getCurrentWindowIndex()); + assertArrayEquals([0, 1], player.shuffleOrder_); + assertEquals(1, states.length); + assertEquals(Player.PlaybackState.BUFFERING, states[0].playbackState); + assertEquals( + Player.DiscontinuityReason.PERIOD_TRANSITION, + states[0].playbackPosition.discontinuityReason); + assertEquals( + Player.DUMMY_MEDIA_ITEM_INFO, states[0].mediaItemsInfo['uuid1']); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests removing items all items. */ + testRemoveMediaItem_removeAll() { + const jsonMessage = createMessage('player.removeItems', + {uuids: ['uuid1', 'uuid0', 'uuid2']}); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.seekToWindow(2, 2000); + player.prepare(); + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + + sendMessage(jsonMessage); + assertInitialState(playerState, 'ENDED'); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals([], player.shuffleOrder_); + assertEquals(Player.PlaybackState.ENDED, player.getPlaybackState()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, []); + }, + + /** Tests moving an item in the queue. */ + testMoveItem() { + let shuffleOrder = [0, 2, 1]; + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid2', + index: 0, + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving the currently playing item in the queue. */ + testMoveItem_currentWindowIndex() { + let shuffleOrder = [0, 2, 1]; + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid2', + index: 0, + shuffleOrder: shuffleOrder, + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToUuid('uuid2', 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); + assertEquals(0, player.getCurrentWindowIndex()); + assertArrayEquals(shuffleOrder, player.shuffleOrder_); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from before to after the currently playing item. */ + testMoveItem_decreaseCurrentWindowIndex() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid0', + index: 5, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5', 'uuid0'], + queue.map((x) => x.uuid)); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from after to before the currently playing item. */ + testMoveItem_increaseCurrentWindowIndex() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid5', + index: 0, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid5', 'uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4'], + queue.map((x) => x.uuid)); + assertEquals(3, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from after to the current window index. */ + testMoveItem_toCurrentWindowIndex_fromAfter() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid5', + index: 2, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid0', 'uuid1', 'uuid5', 'uuid2', 'uuid3', 'uuid4'], + queue.map((x) => x.uuid)); + assertEquals(3, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests moving an item from before to the current window index. */ + testMoveItem_toCurrentWindowIndex_fromBefore() { + const jsonMessage = createMessage('player.moveItem', { + uuid: 'uuid0', + index: 2, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 6)); + player.prepare(); + player.seekToWindow(2, 0); + assertEquals(2, player.getCurrentWindowIndex()); + + assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + sendMessage(jsonMessage); + assertArrayEquals(['uuid1', 'uuid2', 'uuid0', 'uuid3', 'uuid4', 'uuid5'], + queue.map((x) => x.uuid)); + assertEquals(1, player.getCurrentWindowIndex()); + util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); + }, + + /** Tests seekTo. */ + testSeekTo() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'uuid1', + 'positionMs': 2000 + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + sendMessage(jsonMessage); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + }, + + /** Tests seekTo to unknown uuid. */ + testSeekTo_unknownUuid() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'unknown', + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToWindow(1, 2000); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + + sendMessage(jsonMessage); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + }, + + /** Tests seekTo without position. */ + testSeekTo_noPosition_defaultsToZero() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'uuid1', + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + sendMessage(jsonMessage); + assertEquals(0, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + }, + + /** Tests seekTo to negative position. */ + testSeekTo_negativePosition_defaultsToZero() { + const jsonMessage = createMessage('player.seekTo', + { + 'uuid': 'uuid2', + 'positionMs': -1, + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToWindow(1, 2000); + assertEquals(2000, player.getCurrentPositionMs()); + assertEquals(1, player.getCurrentWindowIndex()); + + sendMessage(jsonMessage); + assertEquals(0, player.getCurrentPositionMs()); + assertEquals(2, player.getCurrentWindowIndex()); + }, + + /** Tests whether validation is turned on. */ + testMediaItemValidation_isOn() { + const index = 0; + const mediaItem = createMediaItem('99'); + delete mediaItem.uuid; + const jsonMessage = createMessage('player.addItems', { + index: index, + items: [mediaItem], + shuffleOrder: [], + }); + + sendMessage(jsonMessage); + assertEquals(0, queue.length); + }, + + /** Tests whether the state is sent to sender apps on state transition. */ + testPlayerStateIsSent_withCorrectSequenceNumber() { + assertUndefined(mocks.state().outputMessages['sender0']); + const playMessage = + createMessage('player.setPlayWhenReady', {playWhenReady: true}); + sendMessage(playMessage); + + const playerState = mocks.state().outputMessages['sender0'][0]; + assertTrue(playerState.playWhenReady); + assertEquals(playMessage.sequenceNumber, playerState.sequenceNumber); + }, + + /** Tests whether a connect of a sender app sends the current player state. */ + testSenderConnection() { + const onSenderConnected = mocks.state().onSenderConnected; + assertTrue(goog.isFunction(onSenderConnected)); + onSenderConnected({senderId: 'sender0'}); + + const playerState = mocks.state().outputMessages['sender0'][0]; + assertEquals(Player.RepeatMode.OFF, playerState.repeatMode); + assertEquals('IDLE', playerState.playbackState); + assertArrayEquals([], playerState.mediaQueue); + assertEquals(-1, playerState.sequenceNumber); + }, + + /** Tests whether a disconnect of a sender notifies the message dispatcher. */ + testSenderDisconnection_callsMessageDispatcher() { + mocks.setUp(); + let notifiedSenderId; + const myPlayer = new Player(mocks.createShakaFake()); + const myManagerFake = mocks.createCastReceiverContextFake(); + new Receiver(myPlayer, myManagerFake, { + registerActionHandler() {}, + notifySenderDisconnected(senderId) { + notifiedSenderId = senderId; + }, + }); + + const onSenderDisconnected = mocks.state().onSenderDisconnected; + assertTrue(goog.isFunction(onSenderDisconnected)); + onSenderDisconnected({senderId: 'sender0'}); + assertEquals('sender0', notifiedSenderId); + }, + + /** + * Tests whether the state right after creation of the player matches + * expectations. + */ + testInitialState() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + assertEquals(0, player.getCurrentPositionMs()); + // Dump a player state to the listener. + player.invalidate(); + // Asserts the state just after creation. + assertInitialState(playerState, 'IDLE'); + }, + + /** Tests whether user properties can be changed when in IDLE state */ + testChangingUserPropertiesWhenIdle() { + mocks.state().isSilent = true; + const states = []; + let counter = 0; + player.addPlayerListener((state) => { + states.push(state); + }); + // Adding items when IDLE. + player.addQueueItems(0, util.queue.slice(0, 3)); + counter++; + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); + assertArrayEquals( + ['uuid0', 'uuid1', 'uuid2'], + states[counter - 1].mediaQueue.map((i) => i.uuid)); + + // Set playWhenReady when IDLE. + assertFalse(player.getPlayWhenReady()); + player.setPlayWhenReady(true); + counter++; + assertTrue(player.getPlayWhenReady()); + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); + + // Seeking when IDLE. + player.seekToUuid('uuid2', 1000); + counter++; + // Window index not set when idle. + assertEquals(2, player.getCurrentWindowIndex()); + assertEquals(1000, player.getCurrentPositionMs()); + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); + // But window index is set when prepared. + player.prepare(); + assertEquals(2, player.getCurrentWindowIndex()); + }, + + /** Tests the state after calling prepare. */ + testPrepare() { + mocks.state().isSilent = true; + const states = []; + let counter = 0; + player.addPlayerListener((state) => { + states.push(state); + }); + const prepareMessage = createMessage('player.prepare', {}); + + player.addQueueItems(0, util.queue.slice(0, 3)); + player.seekToWindow(1, 1000); + counter += 2; + + // Sends prepare message. + sendMessage(prepareMessage); + counter++; + assertEquals(counter, states.length); + assertEquals('uuid1', states[counter - 1].playbackPosition.uuid); + assertEquals( + Player.PlaybackState.BUFFERING, states[counter - 1].playbackState); + + // Fakes Shaka events. + mocks.state().isSilent = false; + mocks.notifyListeners('streaming'); + mocks.notifyListeners('loadeddata'); + counter += 2; + assertEquals(counter, states.length); + assertEquals(Player.PlaybackState.READY, states[counter - 1].playbackState); + }, + + /** Tests stopping the player with `reset=true`. */ + testStop_resetTrue() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + const stopMessage = createMessage('player.stop', {reset: true}); + + player.setRepeatMode(Player.RepeatMode.ALL); + player.setShuffleModeEnabled(true); + player.setPlayWhenReady(true); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + mocks.state().isSilent = false; + mocks.notifyListeners('loadeddata'); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); + assertEquals(0, playerState.windowIndex); + assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); + assertEquals(1, player.playbackType_); + // Stop the player. + sendMessage(stopMessage); + // Asserts the state looks the same as just after creation. + assertInitialState(playerState, 'IDLE'); + assertNull(playerState.playbackPosition); + // Assert player properties are preserved. + assertTrue(playerState.shuffleModeEnabled); + assertTrue(playerState.playWhenReady); + assertEquals(Player.RepeatMode.ALL, playerState.repeatMode); + }, + + /** Tests stopping the player with `reset=false`. */ + testStop_resetFalse() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + const stopMessage = createMessage('player.stop', {reset: false}); + + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + player.seekToUuid('uuid1', 1000); + mocks.state().isSilent = false; + mocks.notifyListeners('streaming'); + mocks.notifyListeners('trackschanged'); + mocks.notifyListeners('loadeddata'); + assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); + assertEquals(1, playerState.windowIndex); + assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); + assertEquals(2, player.playbackType_); + // Stop the player. + sendMessage(stopMessage); + assertEquals('IDLE', playerState.playbackState); + assertUndefined(playerState.playbackError); + // Assert the timeline is preserved. + assertEquals(3, queue.length); + assertEquals(3, playerState.windowCount); + assertEquals(1, player.windowIndex_); + assertEquals(1, playerState.windowIndex); + // Assert the playback position is correct. + assertEquals(1000, playerState.playbackPosition.positionMs); + assertEquals('uuid1', playerState.playbackPosition.uuid); + assertEquals(0, playerState.playbackPosition.periodId); + assertNull(playerState.playbackPosition.discontinuityReason); + assertEquals(1000, player.getCurrentPositionMs()); + // Assert player properties are preserved. + assertEquals(20000, player.getDurationMs()); + assertEquals(2, Object.entries(player.mediaItemInfoMap_).length); + assertEquals(0, player.windowPeriodIndex_); + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(1, player.windowIndex_); + assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); + assertEquals(999, player.playbackType_); + assertEquals('uuid1', player.uuidToPrepare_); + }, + + /** + * Tests the state after having removed the last item in the queue. This + * resolves to the same state like calling `stop(true)` except that the state + * is ENDED and the queue is naturally empty and hence the windowIndex is + * unset. + */ + testRemoveLastQueueItem() { + mocks.state().isSilent = true; + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + const removeAllItemsMessage = createMessage( + 'player.removeItems', {uuids: ['uuid0', 'uuid1', 'uuid2']}); + + player.addQueueItems(0, util.queue.slice(0, 3)); + player.seekToWindow(0, 1000); + player.prepare(); + mocks.state().isSilent = false; + mocks.notifyListeners('loadeddata'); + // Remove all items. + sendMessage(removeAllItemsMessage); + // Assert the state after removal of all items. + assertInitialState(playerState, 'ENDED'); + }, + + /** Tests whether a player state is sent when no item is added. */ + testAddItem_noop() { + mocks.state().isSilent = true; + let playerStates = []; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + const noOpMessage = createMessage('player.addItems', { + index: 0, + items: [ + util.queue[0], + ], + shuffleOrder: [0], + }); + player.addQueueItems(0, [util.queue[0]], []); + player.prepare(); + assertEquals(2, playerStates.length); + assertEquals(2, mocks.state().outputMessages['sender0'].length); + sendMessage(noOpMessage); + assertEquals(2, playerStates.length); + assertEquals(3, mocks.state().outputMessages['sender0'].length); + }, + + /** Tests whether a player state is sent when no item is removed. */ + testRemoveItem_noop() { + mocks.state().isSilent = true; + let playerStates = []; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + const noOpMessage = + createMessage('player.removeItems', {uuids: ['uuid00']}); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + assertEquals(2, playerStates.length); + assertEquals(2, mocks.state().outputMessages['sender0'].length); + sendMessage(noOpMessage); + assertEquals(2, playerStates.length); + assertEquals(3, mocks.state().outputMessages['sender0'].length); + }, + + /** Tests whether a player state is sent when item is not moved. */ + testMoveItem_noop() { + mocks.state().isSilent = true; + let playerStates = []; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + const noOpMessage = createMessage('player.moveItem', { + uuid: 'uuid00', + index: 0, + shuffleOrder: [], + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + assertEquals(2, playerStates.length); + assertEquals(2, mocks.state().outputMessages['sender0'].length); + sendMessage(noOpMessage); + assertEquals(2, playerStates.length); + assertEquals(3, mocks.state().outputMessages['sender0'].length); + }, + + /** Tests whether playback actions send a state when no-op */ + testNoOpPlaybackActionsSendPlayerState() { + mocks.state().isSilent = true; + let playerStates = []; + let parsedMessage; + player.addPlayerListener((state) => { + playerStates.push(state); + }); + player.addQueueItems(0, util.queue.slice(0, 3)); + player.prepare(); + + const outputMessages = mocks.state().outputMessages['sender0']; + const setupMessageCount = playerStates.length; + let totalMessageCount = setupMessageCount; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + + const firstNoOpMessage = createMessage('player.setPlayWhenReady', { + playWhenReady: false, + }); + let expectedSequenceNumber = firstNoOpMessage.sequenceNumber; + + sendMessage(firstNoOpMessage); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + + sendMessage(createMessage('player.setRepeatMode', { + repeatMode: 'OFF', + })); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + + sendMessage(createMessage('player.setShuffleModeEnabled', { + shuffleModeEnabled: false, + })); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + + sendMessage(createMessage('player.seekTo', { + uuid: 'not_existing', + positionMs: 0, + })); + totalMessageCount++; + assertEquals(setupMessageCount, playerStates.length); + assertEquals(totalMessageCount, outputMessages.length); + parsedMessage = outputMessages[totalMessageCount - 1]; + assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); + }, +}); diff --git a/cast_receiver_app/test/shaka_error_handling_test.js b/cast_receiver_app/test/shaka_error_handling_test.js new file mode 100644 index 0000000000..a7dafd3176 --- /dev/null +++ b/cast_receiver_app/test/shaka_error_handling_test.js @@ -0,0 +1,84 @@ +/** + * 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. + * + * @fileoverview Unit tests for playback methods. + */ + +goog.module('exoplayer.cast.test.shaka'); +goog.setTestOnly(); + +const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); +const Player = goog.require('exoplayer.cast.Player'); +const mocks = goog.require('exoplayer.cast.test.mocks'); +const testSuite = goog.require('goog.testing.testSuite'); +const util = goog.require('exoplayer.cast.test.util'); + +let player; +let shakaFake; + +testSuite({ + setUp() { + mocks.setUp(); + shakaFake = mocks.createShakaFake(); + player = new Player(shakaFake, new ConfigurationFactory()); + }, + + /** Tests Shaka critical error handling on load. */ + async testShakaCriticalError_onload() { + mocks.state().isSilent = true; + mocks.state().setShakaThrowsOnLoad(true); + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + player.addQueueItems(0, util.queue.slice(0, 2)); + player.seekToUuid('uuid1', 2000); + player.setPlayWhenReady(true); + // Calling prepare triggers a critical Shaka error. + await player.prepare(); + // Assert player state after error. + assertEquals('IDLE', playerState.playbackState); + assertEquals(mocks.state().shakaError.category, playerState.error.category); + assertEquals(mocks.state().shakaError.code, playerState.error.code); + assertEquals( + 'loading failed for uri: http://example1.com', + playerState.error.message); + assertEquals(999, player.playbackType_); + // Assert player properties are preserved. + assertEquals(2000, player.getCurrentPositionMs()); + assertTrue(player.getPlayWhenReady()); + assertEquals(1, player.getCurrentWindowIndex()); + assertEquals(1, player.windowIndex_); + }, + + /** Tests Shaka critical error handling on unload. */ + async testShakaCriticalError_onunload() { + mocks.state().isSilent = true; + mocks.state().setShakaThrowsOnUnload(true); + let playerState; + player.addPlayerListener((state) => { + playerState = state; + }); + player.addQueueItems(0, util.queue.slice(0, 2)); + player.setPlayWhenReady(true); + assertUndefined(player.videoElement_.src); + // Calling prepare triggers a critical Shaka error. + await player.prepare(); + // Assert player state after caught and ignored error. + await assertEquals('BUFFERING', playerState.playbackState); + assertEquals('http://example.com', player.videoElement_.src); + assertEquals(1, player.playbackType_); + }, +}); diff --git a/cast_receiver_app/test/util.js b/cast_receiver_app/test/util.js new file mode 100644 index 0000000000..22244675b7 --- /dev/null +++ b/cast_receiver_app/test/util.js @@ -0,0 +1,87 @@ +/** + * 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. + * + * @fileoverview Description of this file. + */ + +goog.module('exoplayer.cast.test.util'); +goog.setTestOnly(); + +/** + * The queue of sample media items + * + * @type {!Array} + */ +const queue = [ + { + uuid: 'uuid0', + media: { + uri: 'http://example.com', + }, + mimeType: 'video/*', + }, + { + uuid: 'uuid1', + media: { + uri: 'http://example1.com', + }, + mimeType: 'application/dash+xml', + }, + { + uuid: 'uuid2', + media: { + uri: 'http://example2.com', + }, + mimeType: 'video/*', + }, + { + uuid: 'uuid3', + media: { + uri: 'http://example3.com', + }, + mimeType: 'application/dash+xml', + }, + { + uuid: 'uuid4', + media: { + uri: 'http://example4.com', + }, + mimeType: 'video/*', + }, + { + uuid: 'uuid5', + media: { + uri: 'http://example5.com', + }, + mimeType: 'application/dash+xml', + }, +]; + +/** + * Asserts whether the map of uuids is complete and points to the correct + * indices. + * + * @param {!Object} uuidIndexMap The uuid to index map. + * @param {!Array} queue The media item queue. + */ +const assertUuidIndexMap = (uuidIndexMap, queue) => { + assertEquals(queue.length, Object.entries(uuidIndexMap).length); + queue.forEach((mediaItem, index) => { + assertEquals(uuidIndexMap[mediaItem.uuid], index); + }); +}; + +exports.queue = queue; +exports.assertUuidIndexMap = assertUuidIndexMap; diff --git a/cast_receiver_app/test/validation_test.js b/cast_receiver_app/test/validation_test.js new file mode 100644 index 0000000000..8e58185cfa --- /dev/null +++ b/cast_receiver_app/test/validation_test.js @@ -0,0 +1,266 @@ +/** + * 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. + * + * @fileoverview Unit tests for queue manipulations. + */ + +goog.module('exoplayer.cast.test.validation'); +goog.setTestOnly(); + +const testSuite = goog.require('goog.testing.testSuite'); +const validation = goog.require('exoplayer.cast.validation'); + +/** + * Creates a sample drm media for validation tests. + * + * @return {!Object} A dummy media item with a drm scheme. + */ +const createDrmMedia = function() { + return { + uuid: 'string', + media: { + uri: 'string', + }, + mimeType: 'application/dash+xml', + drmSchemes: [ + { + uuid: 'string', + licenseServer: { + uri: 'string', + requestHeaders: { + 'string': 'string', + }, + }, + }, + ], + }; +}; + +testSuite({ + + /** Tests minimal valid media item. */ + testValidateMediaItem_minimal() { + const mediaItem = { + uuid: 'string', + media: { + uri: 'string', + }, + mimeType: 'application/dash+xml', + }; + assertTrue(validation.validateMediaItem(mediaItem)); + + const uuid = mediaItem.uuid; + delete mediaItem.uuid; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.uuid = uuid; + assertTrue(validation.validateMediaItem(mediaItem)); + + const mimeType = mediaItem.mimeType; + delete mediaItem.mimeType; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.mimeType = mimeType; + assertTrue(validation.validateMediaItem(mediaItem)); + + const media = mediaItem.media; + delete mediaItem.media; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.media = media; + assertTrue(validation.validateMediaItem(mediaItem)); + + const uri = mediaItem.media.uri; + delete mediaItem.media.uri; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.media.uri = uri; + assertTrue(validation.validateMediaItem(mediaItem)); + }, + + /** Tests media item drm property validation. */ + testValidateMediaItem_drmSchemes() { + const mediaItem = createDrmMedia(); + assertTrue(validation.validateMediaItem(mediaItem)); + + const uuid = mediaItem.drmSchemes[0].uuid; + delete mediaItem.drmSchemes[0].uuid; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.drmSchemes[0].uuid = uuid; + assertTrue(validation.validateMediaItem(mediaItem)); + + const licenseServer = mediaItem.drmSchemes[0].licenseServer; + delete mediaItem.drmSchemes[0].licenseServer; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.drmSchemes[0].licenseServer = licenseServer; + assertTrue(validation.validateMediaItem(mediaItem)); + + const uri = mediaItem.drmSchemes[0].licenseServer.uri; + delete mediaItem.drmSchemes[0].licenseServer.uri; + assertFalse(validation.validateMediaItem(mediaItem)); + mediaItem.drmSchemes[0].licenseServer.uri = uri; + assertTrue(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validation of startPositionUs and endPositionUs. */ + testValidateMediaItem_endAndStartPositionUs() { + const mediaItem = createDrmMedia(); + + mediaItem.endPositionUs = 0; + mediaItem.startPositionUs = 120 * 1000; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.endPositionUs = '0'; + assertFalse(validation.validateMediaItem(mediaItem)); + + mediaItem.endPositionUs = 0; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.startPositionUs = true; + assertFalse(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validation of the title. */ + testValidateMediaItem_title() { + const mediaItem = createDrmMedia(); + + mediaItem.title = '0'; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.title = 0; + assertFalse(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validation of the description. */ + testValidateMediaItem_description() { + const mediaItem = createDrmMedia(); + + mediaItem.description = '0'; + assertTrue(validation.validateMediaItem(mediaItem)); + + mediaItem.description = 0; + assertFalse(validation.validateMediaItem(mediaItem)); + }, + + /** Tests validating property of type string. */ + testValidateProperty_string() { + const obj = { + field: 'string', + }; + assertTrue(validation.validateProperty(obj, 'field', 'string')); + assertTrue(validation.validateProperty(obj, 'field', '?string')); + + obj.field = 0; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertFalse(validation.validateProperty(obj, 'field', '?string')); + + obj.field = true; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertFalse(validation.validateProperty(obj, 'field', '?string')); + + obj.field = {}; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertFalse(validation.validateProperty(obj, 'field', '?string')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'string')); + assertTrue(validation.validateProperty(obj, 'field', '?string')); + }, + + /** Tests validating property of type number. */ + testValidateProperty_number() { + const obj = { + field: 0, + }; + assertTrue(validation.validateProperty(obj, 'field', 'number')); + assertTrue(validation.validateProperty(obj, 'field', '?number')); + + obj.field = '0'; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertFalse(validation.validateProperty(obj, 'field', '?number')); + + obj.field = true; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertFalse(validation.validateProperty(obj, 'field', '?number')); + + obj.field = {}; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertFalse(validation.validateProperty(obj, 'field', '?number')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'number')); + assertTrue(validation.validateProperty(obj, 'field', '?number')); + }, + + /** Tests validating property of type boolean. */ + testValidateProperty_boolean() { + const obj = { + field: true, + }; + assertTrue(validation.validateProperty(obj, 'field', 'boolean')); + assertTrue(validation.validateProperty(obj, 'field', '?boolean')); + + obj.field = '0'; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertFalse(validation.validateProperty(obj, 'field', '?boolean')); + + obj.field = 1000; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertFalse(validation.validateProperty(obj, 'field', '?boolean')); + + obj.field = [true]; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertFalse(validation.validateProperty(obj, 'field', '?boolean')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'boolean')); + assertTrue(validation.validateProperty(obj, 'field', '?boolean')); + }, + + /** Tests validating property of type array. */ + testValidateProperty_array() { + const obj = { + field: [], + }; + assertTrue(validation.validateProperty(obj, 'field', 'Array')); + assertTrue(validation.validateProperty(obj, 'field', '?Array')); + + obj.field = '0'; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertFalse(validation.validateProperty(obj, 'field', '?Array')); + + obj.field = 1000; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertFalse(validation.validateProperty(obj, 'field', '?Array')); + + obj.field = true; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertFalse(validation.validateProperty(obj, 'field', '?Array')); + + delete obj.field; + assertFalse(validation.validateProperty(obj, 'field', 'Array')); + assertTrue(validation.validateProperty(obj, 'field', '?Array')); + }, + + /** Tests validating properties of type RepeatMode */ + testValidateProperty_repeatMode() { + const obj = { + off: 'OFF', + one: 'ONE', + all: 'ALL', + invalid: 'invalid', + }; + assertTrue(validation.validateProperty(obj, 'off', 'RepeatMode')); + assertTrue(validation.validateProperty(obj, 'one', 'RepeatMode')); + assertTrue(validation.validateProperty(obj, 'all', 'RepeatMode')); + assertFalse(validation.validateProperty(obj, 'invalid', 'RepeatMode')); + }, +}); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java new file mode 100644 index 0000000000..e8ad2c1a0d --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java @@ -0,0 +1,421 @@ +/* + * 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.castdemo; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import android.view.KeyEvent; +import android.view.View; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.ext.cast.DefaultCastSessionManager; +import com.google.android.exoplayer2.ext.cast.ExoCastPlayer; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +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.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; + +/** Manages players and an internal media queue for the Cast demo app. */ +/* package */ class ExoCastPlayerManager + implements PlayerManager, EventListener, SessionAvailabilityListener { + + private static final String TAG = "ExoCastPlayerManager"; + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); + + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final SimpleExoPlayer exoPlayer; + private final ExoCastPlayer exoCastPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; + + private int currentItemIndex; + private Player currentPlayer; + + /** + * Creates a new manager for {@link SimpleExoPlayer} and {@link ExoCastPlayer}. + * + * @param listener A {@link Listener}. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. + */ + public ExoCastPlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); + + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); + + exoCastPlayer = + new ExoCastPlayer( + sessionManagerListener -> + new DefaultCastSessionManager(castContext, sessionManagerListener)); + exoCastPlayer.addListener(this); + exoCastPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(exoCastPlayer); + + setCurrentPlayer(exoCastPlayer.isCastSessionAvailable() ? exoCastPlayer : exoPlayer); + } + + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + @Override + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + @Override + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + @Override + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.addItemsToQueue(item); + } + } + + /** Returns the size of the media queue. */ + @Override + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + @Override + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + @Override + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + // This may happen if another sender app removes items while this sender app is in "swiping + // an item" state. + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + mediaQueue.remove(itemIndex); + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.removeItemFromQueue(itemIndex); + } + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param item The item to move. This method does nothing if {@code item} is not contained in the + * queue. + * @param toIndex The target index of the item in the queue. If {@code toIndex} exceeds the last + * position in the queue, {@code toIndex} is clamped to match the largest possible value. + * @return True if {@code item} was contained in the queue, and {@code toIndex} was a valid + * position. False otherwise. + */ + @Override + public boolean moveItem(MediaItem item, int toIndex) { + int indexOfItem = mediaQueue.indexOf(item); + if (indexOfItem == -1) { + // This may happen if another sender app removes items while this sender app is in "dragging + // an item" state. + return false; + } + int clampedToIndex = Math.min(toIndex, mediaQueue.size() - 1); + mediaQueue.add(clampedToIndex, mediaQueue.remove(indexOfItem)); + concatenatingMediaSource.moveMediaSource(indexOfItem, clampedToIndex); + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.moveItemInQueue(indexOfItem, clampedToIndex); + } + // Index update. + maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); + return clampedToIndex == toIndex; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == exoCastPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + @Override + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + exoCastPlayer.setSessionAvailabilityListener(null); + exoCastPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + if (currentPlayer == exoCastPlayer && reason != Player.TIMELINE_CHANGE_REASON_RESET) { + maybeUpdateLocalQueueWithRemoteQueueAndNotify(); + } + updateCurrentItemIndex(); + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + Log.e(TAG, "The player encountered an error.", error); + listener.onPlayerError(); + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(exoCastPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void maybeUpdateLocalQueueWithRemoteQueueAndNotify() { + Assertions.checkState(currentPlayer == exoCastPlayer); + boolean mediaQueuesMatch = mediaQueue.size() == exoCastPlayer.getQueueSize(); + for (int i = 0; mediaQueuesMatch && i < mediaQueue.size(); i++) { + mediaQueuesMatch = mediaQueue.get(i).uuid.equals(exoCastPlayer.getQueueItem(i).uuid); + } + if (mediaQueuesMatch) { + // The media queues match. Do nothing. + return; + } + mediaQueue.clear(); + concatenatingMediaSource.clear(); + for (int i = 0; i < exoCastPlayer.getQueueSize(); i++) { + MediaItem item = exoCastPlayer.getQueueItem(i); + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + } + listener.onQueueContentsExternallyChanged(); + } + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == exoCastPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + if (this.currentPlayer != null) { + int playbackState = this.currentPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = this.currentPlayer.getCurrentPosition(); + playWhenReady = this.currentPlayer.getPlayWhenReady(); + windowIndex = this.currentPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + this.currentPlayer.stop(true); + } else { + // This is the initial setup. No need to save any state. + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + boolean shouldSeekInNewCurrentPlayer; + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + shouldSeekInNewCurrentPlayer = true; + } else /* currentPlayer == exoCastPlayer */ { + if (exoCastPlayer.getPlaybackState() == Player.STATE_IDLE) { + exoCastPlayer.prepare(); + } + if (mediaQueue.isEmpty()) { + // Casting started with no local queue. We take the receiver app's queue as our own. + maybeUpdateLocalQueueWithRemoteQueueAndNotify(); + shouldSeekInNewCurrentPlayer = false; + } else { + // Casting started when the sender app had no queue. We just load our items into the + // receiver app's queue. If the receiver had no items in its queue, we also seek to wherever + // the sender app was playing. + int currentExoCastPlayerState = exoCastPlayer.getPlaybackState(); + shouldSeekInNewCurrentPlayer = + currentExoCastPlayerState == Player.STATE_IDLE + || currentExoCastPlayerState == Player.STATE_ENDED; + exoCastPlayer.addItemsToQueue(mediaQueue.toArray(new MediaItem[0])); + } + } + + // Playback transition. + if (shouldSeekInNewCurrentPlayer && windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } else if (getMediaQueueSize() > 0) { + maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + currentPlayer.seekTo(itemIndex, positionMs); + if (currentPlayer.getPlaybackState() == Player.STATE_IDLE) { + if (currentPlayer == exoCastPlayer) { + exoCastPlayer.prepare(); + } else { + exoPlayer.prepare(concatenatingMediaSource); + } + } + currentPlayer.setPlayWhenReady(playWhenReady); + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private static MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.media.uri; + switch (item.mimeType) { + case DemoUtil.MIME_TYPE_SS: + return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_DASH: + return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_VIDEO_MP4: + return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + default: + { + throw new IllegalStateException("Unsupported type: " + item.mimeType); + } + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java new file mode 100644 index 0000000000..7c1f06e8d2 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java @@ -0,0 +1,86 @@ +/* + * 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; + +/** Handles communication with the receiver app using a cast session. */ +public interface CastSessionManager { + + /** Factory for {@link CastSessionManager} instances. */ + interface Factory { + + /** + * Creates a {@link CastSessionManager} instance with the given listener. + * + * @param listener The listener to notify on receiver app and session state updates. + * @return The created instance. + */ + CastSessionManager create(StateListener listener); + } + + /** + * Extends {@link SessionAvailabilityListener} by adding receiver app state notifications. + * + *

      Receiver app state notifications contain a sequence number that matches the sequence number + * of the last {@link ExoCastMessage} sent (using {@link #send(ExoCastMessage)}) by this session + * manager and processed by the receiver app. Sequence numbers are non-negative numbers. + */ + interface StateListener extends SessionAvailabilityListener { + + /** + * Called when a status update is received from the Cast Receiver app. + * + * @param stateUpdate A {@link ReceiverAppStateUpdate} containing the fields included in the + * message. + */ + void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate); + } + + /** + * Special constant representing an unset sequence number. It is guaranteed to be a negative + * value. + */ + long SEQUENCE_NUMBER_UNSET = Long.MIN_VALUE; + + /** + * Connects the session manager to the cast message bus and starts listening for session + * availability changes. Also announces that this sender app is connected to the message bus. + */ + void start(); + + /** Stops tracking the state of the cast session and closes any existing session. */ + void stopTrackingSession(); + + /** + * Same as {@link #stopTrackingSession()}, but also stops the receiver app if a session is + * currently available. + */ + void stopTrackingSessionAndCasting(); + + /** Whether a cast session is available. */ + boolean isCastSessionAvailable(); + + /** + * Sends an {@link ExoCastMessage} to the receiver app. + * + *

      A sequence number is assigned to every sent message. Message senders may mask the local + * state until a status update from the receiver app (see {@link StateListener}) is received with + * a greater or equal sequence number. + * + * @param message The message to send. + * @return The sequence number assigned to the message. + */ + long send(ExoCastMessage message); +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java new file mode 100644 index 0000000000..c08a9bc352 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java @@ -0,0 +1,187 @@ +/* + * 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; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.gms.cast.Cast; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.cast.framework.CastSession; +import com.google.android.gms.cast.framework.SessionManager; +import com.google.android.gms.cast.framework.SessionManagerListener; +import java.io.IOException; +import org.json.JSONException; + +/** Implements {@link CastSessionManager} by using JSON message passing. */ +public class DefaultCastSessionManager implements CastSessionManager { + + private static final String TAG = "DefaultCastSessionManager"; + private static final String EXOPLAYER_CAST_NAMESPACE = "urn:x-cast:com.google.exoplayer.cast"; + + private final SessionManager sessionManager; + private final CastSessionListener castSessionListener; + private final StateListener stateListener; + private final Cast.MessageReceivedCallback messageReceivedCallback; + + private boolean started; + private long sequenceNumber; + private long expectedInitialStateUpdateSequence; + @Nullable private CastSession currentSession; + + /** + * @param context The Cast context from which the cast session is obtained. + * @param stateListener The listener to notify of state changes. + */ + public DefaultCastSessionManager(CastContext context, StateListener stateListener) { + this.stateListener = stateListener; + sessionManager = context.getSessionManager(); + currentSession = sessionManager.getCurrentCastSession(); + castSessionListener = new CastSessionListener(); + messageReceivedCallback = new CastMessageCallback(); + expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; + } + + @Override + public void start() { + started = true; + sessionManager.addSessionManagerListener(castSessionListener, CastSession.class); + currentSession = sessionManager.getCurrentCastSession(); + if (currentSession != null) { + setMessageCallbackOnSession(); + } + } + + @Override + public void stopTrackingSession() { + stop(/* stopCasting= */ false); + } + + @Override + public void stopTrackingSessionAndCasting() { + stop(/* stopCasting= */ true); + } + + @Override + public boolean isCastSessionAvailable() { + return currentSession != null && expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET; + } + + @Override + public long send(ExoCastMessage message) { + if (currentSession != null) { + currentSession.sendMessage(EXOPLAYER_CAST_NAMESPACE, message.toJsonString(sequenceNumber)); + } else { + Log.w(TAG, "Tried to send a message with no established session. Method: " + message.method); + } + return sequenceNumber++; + } + + private void stop(boolean stopCasting) { + sessionManager.removeSessionManagerListener(castSessionListener, CastSession.class); + if (currentSession != null) { + sessionManager.endCurrentSession(stopCasting); + } + currentSession = null; + started = false; + } + + private void setCastSession(@Nullable CastSession session) { + Assertions.checkState(started); + boolean hadSession = currentSession != null; + currentSession = session; + if (!hadSession && session != null) { + setMessageCallbackOnSession(); + } else if (hadSession && session == null) { + stateListener.onCastSessionUnavailable(); + } + } + + private void setMessageCallbackOnSession() { + try { + Assertions.checkNotNull(currentSession) + .setMessageReceivedCallbacks(EXOPLAYER_CAST_NAMESPACE, messageReceivedCallback); + expectedInitialStateUpdateSequence = send(new ExoCastMessage.OnClientConnected()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** Listens for Cast session state changes. */ + private class CastSessionListener implements SessionManagerListener { + + @Override + public void onSessionStarting(CastSession castSession) {} + + @Override + public void onSessionStarted(CastSession castSession, String sessionId) { + setCastSession(castSession); + } + + @Override + public void onSessionStartFailed(CastSession castSession, int error) {} + + @Override + public void onSessionEnding(CastSession castSession) {} + + @Override + public void onSessionEnded(CastSession castSession, int error) { + setCastSession(null); + } + + @Override + public void onSessionResuming(CastSession castSession, String sessionId) {} + + @Override + public void onSessionResumed(CastSession castSession, boolean wasSuspended) { + setCastSession(castSession); + } + + @Override + public void onSessionResumeFailed(CastSession castSession, int error) {} + + @Override + public void onSessionSuspended(CastSession castSession, int reason) { + setCastSession(null); + } + } + + private class CastMessageCallback implements Cast.MessageReceivedCallback { + + @Override + public void onMessageReceived(CastDevice castDevice, String namespace, String message) { + if (!EXOPLAYER_CAST_NAMESPACE.equals(namespace)) { + // Non-matching namespace. Ignore. + Log.e(TAG, String.format("Unrecognized namespace: '%s'.", namespace)); + return; + } + try { + ReceiverAppStateUpdate receivedUpdate = ReceiverAppStateUpdate.fromJsonMessage(message); + if (expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET + || receivedUpdate.sequenceNumber >= expectedInitialStateUpdateSequence) { + stateListener.onStateUpdateFromReceiverApp(receivedUpdate); + if (expectedInitialStateUpdateSequence != SEQUENCE_NUMBER_UNSET) { + expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; + stateListener.onCastSessionAvailable(); + } + } + } catch (JSONException e) { + Log.e(TAG, "Error while parsing state update from receiver: ", e); + } + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java new file mode 100644 index 0000000000..36173bfc5d --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java @@ -0,0 +1,118 @@ +/* + * 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; + +/** Defines constants used by the Cast extension. */ +public final class ExoCastConstants { + + private ExoCastConstants() {} + + public static final int PROTOCOL_VERSION = 0; + + // String representations. + + public static final String STR_STATE_IDLE = "IDLE"; + public static final String STR_STATE_BUFFERING = "BUFFERING"; + public static final String STR_STATE_READY = "READY"; + public static final String STR_STATE_ENDED = "ENDED"; + + public static final String STR_REPEAT_MODE_OFF = "OFF"; + public static final String STR_REPEAT_MODE_ONE = "ONE"; + public static final String STR_REPEAT_MODE_ALL = "ALL"; + + public static final String STR_DISCONTINUITY_REASON_PERIOD_TRANSITION = "PERIOD_TRANSITION"; + public static final String STR_DISCONTINUITY_REASON_SEEK = "SEEK"; + public static final String STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT = "SEEK_ADJUSTMENT"; + public static final String STR_DISCONTINUITY_REASON_AD_INSERTION = "AD_INSERTION"; + public static final String STR_DISCONTINUITY_REASON_INTERNAL = "INTERNAL"; + + public static final String STR_SELECTION_FLAG_DEFAULT = "DEFAULT"; + public static final String STR_SELECTION_FLAG_FORCED = "FORCED"; + public static final String STR_SELECTION_FLAG_AUTOSELECT = "AUTOSELECT"; + + // Methods. + + public static final String METHOD_BASE = "player."; + + public static final String METHOD_ON_CLIENT_CONNECTED = METHOD_BASE + "onClientConnected"; + public static final String METHOD_ADD_ITEMS = METHOD_BASE + "addItems"; + public static final String METHOD_MOVE_ITEM = METHOD_BASE + "moveItem"; + public static final String METHOD_PREPARE = METHOD_BASE + "prepare"; + public static final String METHOD_REMOVE_ITEMS = METHOD_BASE + "removeItems"; + public static final String METHOD_SET_PLAY_WHEN_READY = METHOD_BASE + "setPlayWhenReady"; + public static final String METHOD_SET_REPEAT_MODE = METHOD_BASE + "setRepeatMode"; + public static final String METHOD_SET_SHUFFLE_MODE_ENABLED = + METHOD_BASE + "setShuffleModeEnabled"; + public static final String METHOD_SEEK_TO = METHOD_BASE + "seekTo"; + public static final String METHOD_SET_PLAYBACK_PARAMETERS = METHOD_BASE + "setPlaybackParameters"; + public static final String METHOD_SET_TRACK_SELECTION_PARAMETERS = + METHOD_BASE + ".setTrackSelectionParameters"; + public static final String METHOD_STOP = METHOD_BASE + "stop"; + + // JSON message keys. + + public static final String KEY_ARGS = "args"; + public static final String KEY_DEFAULT_START_POSITION_US = "defaultStartPositionUs"; + public static final String KEY_DESCRIPTION = "description"; + public static final String KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS = + "disabledTextTrackSelectionFlags"; + public static final String KEY_DISCONTINUITY_REASON = "discontinuityReason"; + public static final String KEY_DRM_SCHEMES = "drmSchemes"; + public static final String KEY_DURATION_US = "durationUs"; + public static final String KEY_END_POSITION_US = "endPositionUs"; + public static final String KEY_ERROR_MESSAGE = "error"; + public static final String KEY_ID = "id"; + public static final String KEY_INDEX = "index"; + public static final String KEY_IS_DYNAMIC = "isDynamic"; + public static final String KEY_IS_LOADING = "isLoading"; + public static final String KEY_IS_SEEKABLE = "isSeekable"; + public static final String KEY_ITEMS = "items"; + public static final String KEY_LICENSE_SERVER = "licenseServer"; + public static final String KEY_MEDIA = "media"; + public static final String KEY_MEDIA_ITEMS_INFO = "mediaItemsInfo"; + public static final String KEY_MEDIA_QUEUE = "mediaQueue"; + public static final String KEY_METHOD = "method"; + public static final String KEY_MIME_TYPE = "mimeType"; + public static final String KEY_PERIOD_ID = "periodId"; + public static final String KEY_PERIODS = "periods"; + public static final String KEY_PITCH = "pitch"; + public static final String KEY_PLAY_WHEN_READY = "playWhenReady"; + public static final String KEY_PLAYBACK_PARAMETERS = "playbackParameters"; + public static final String KEY_PLAYBACK_POSITION = "playbackPosition"; + public static final String KEY_PLAYBACK_STATE = "playbackState"; + public static final String KEY_POSITION_IN_FIRST_PERIOD_US = "positionInFirstPeriodUs"; + public static final String KEY_POSITION_MS = "positionMs"; + public static final String KEY_PREFERRED_AUDIO_LANGUAGE = "preferredAudioLanguage"; + public static final String KEY_PREFERRED_TEXT_LANGUAGE = "preferredTextLanguage"; + public static final String KEY_PROTOCOL_VERSION = "protocolVersion"; + public static final String KEY_REPEAT_MODE = "repeatMode"; + public static final String KEY_REQUEST_HEADERS = "requestHeaders"; + public static final String KEY_RESET = "reset"; + public static final String KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE = + "selectUndeterminedTextLanguage"; + public static final String KEY_SEQUENCE_NUMBER = "sequenceNumber"; + public static final String KEY_SHUFFLE_MODE_ENABLED = "shuffleModeEnabled"; + public static final String KEY_SHUFFLE_ORDER = "shuffleOrder"; + public static final String KEY_SKIP_SILENCE = "skipSilence"; + public static final String KEY_SPEED = "speed"; + public static final String KEY_START_POSITION_US = "startPositionUs"; + public static final String KEY_TITLE = "title"; + public static final String KEY_TRACK_SELECTION_PARAMETERS = "trackSelectionParameters"; + public static final String KEY_URI = "uri"; + public static final String KEY_UUID = "uuid"; + public static final String KEY_UUIDS = "uuids"; + public static final String KEY_WINDOW_DURATION_US = "windowDurationUs"; +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java new file mode 100644 index 0000000000..1529e9f5ac --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java @@ -0,0 +1,474 @@ +/* + * 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; + +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PROTOCOL_VERSION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_RESET; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ON_CLIENT_CONNECTED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_PREPARE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_STOP; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.PROTOCOL_VERSION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_DEFAULT; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +// TODO(Internal b/118432277): Evaluate using a proto for sending to the receiver app. +/** A serializable message for operating a media player. */ +public abstract class ExoCastMessage { + + /** Notifies the receiver app of the connection of a sender app to the message bus. */ + public static final class OnClientConnected extends ExoCastMessage { + + public OnClientConnected() { + super(METHOD_ON_CLIENT_CONNECTED); + } + + @Override + protected JSONObject getArgumentsAsJsonObject() { + // No arguments needed. + return new JSONObject(); + } + } + + /** Transitions the player out of {@link Player#STATE_IDLE}. */ + public static final class Prepare extends ExoCastMessage { + + public Prepare() { + super(METHOD_PREPARE); + } + + @Override + protected JSONObject getArgumentsAsJsonObject() { + // No arguments needed. + return new JSONObject(); + } + } + + /** Transitions the player to {@link Player#STATE_IDLE} and optionally resets its state. */ + public static final class Stop extends ExoCastMessage { + + /** Whether the player state should be reset. */ + public final boolean reset; + + public Stop(boolean reset) { + super(METHOD_STOP); + this.reset = reset; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_RESET, reset); + } + } + + /** Adds items to a media player queue. */ + public static final class AddItems extends ExoCastMessage { + + /** + * The index at which the {@link #items} should be inserted. If {@link C#INDEX_UNSET}, the items + * are appended to the queue. + */ + public final int index; + /** The {@link MediaItem items} to add to the media queue. */ + public final List items; + /** + * The shuffle order to use for the media queue that results of adding the items to the queue. + */ + public final ShuffleOrder shuffleOrder; + + /** + * @param index See {@link #index}. + * @param items See {@link #items}. + * @param shuffleOrder See {@link #shuffleOrder}. + */ + public AddItems(int index, List items, ShuffleOrder shuffleOrder) { + super(METHOD_ADD_ITEMS); + this.index = index; + this.items = Collections.unmodifiableList(new ArrayList<>(items)); + this.shuffleOrder = shuffleOrder; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + JSONObject arguments = + new JSONObject() + .put(KEY_ITEMS, getItemsAsJsonArray()) + .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); + maybePutValue(arguments, KEY_INDEX, index, C.INDEX_UNSET); + return arguments; + } + + private JSONArray getItemsAsJsonArray() throws JSONException { + JSONArray result = new JSONArray(); + for (MediaItem item : items) { + result.put(mediaItemAsJsonObject(item)); + } + return result; + } + } + + /** Moves an item in a player media queue. */ + public static final class MoveItem extends ExoCastMessage { + + /** The {@link MediaItem#uuid} of the item to move. */ + public final UUID uuid; + /** The index in the queue to which the item should be moved. */ + public final int index; + /** The shuffle order to use for the media queue that results of moving the item. */ + public ShuffleOrder shuffleOrder; + + /** + * @param uuid See {@link #uuid}. + * @param index See {@link #index}. + * @param shuffleOrder See {@link #shuffleOrder}. + */ + public MoveItem(UUID uuid, int index, ShuffleOrder shuffleOrder) { + super(METHOD_MOVE_ITEM); + this.uuid = uuid; + this.index = index; + this.shuffleOrder = shuffleOrder; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject() + .put(KEY_UUID, uuid) + .put(KEY_INDEX, index) + .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); + } + } + + /** Removes items from a player queue. */ + public static final class RemoveItems extends ExoCastMessage { + + /** The {@link MediaItem#uuid} of the items to remove from the queue. */ + public final List uuids; + + /** @param uuids See {@link #uuids}. */ + public RemoveItems(List uuids) { + super(METHOD_REMOVE_ITEMS); + this.uuids = Collections.unmodifiableList(new ArrayList<>(uuids)); + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_UUIDS, new JSONArray(uuids)); + } + } + + /** See {@link Player#setPlayWhenReady(boolean)}. */ + public static final class SetPlayWhenReady extends ExoCastMessage { + + /** The {@link Player#setPlayWhenReady(boolean) playWhenReady} value to set. */ + public final boolean playWhenReady; + + /** @param playWhenReady See {@link #playWhenReady}. */ + public SetPlayWhenReady(boolean playWhenReady) { + super(METHOD_SET_PLAY_WHEN_READY); + this.playWhenReady = playWhenReady; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_PLAY_WHEN_READY, playWhenReady); + } + } + + /** + * Sets the repeat mode of the media player. + * + * @see Player#setRepeatMode(int) + */ + public static final class SetRepeatMode extends ExoCastMessage { + + /** The {@link Player#setRepeatMode(int) repeatMode} to set. */ + @Player.RepeatMode public final int repeatMode; + + /** @param repeatMode See {@link #repeatMode}. */ + public SetRepeatMode(@Player.RepeatMode int repeatMode) { + super(METHOD_SET_REPEAT_MODE); + this.repeatMode = repeatMode; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_REPEAT_MODE, repeatModeToString(repeatMode)); + } + + private static String repeatModeToString(@Player.RepeatMode int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_OFF: + return STR_REPEAT_MODE_OFF; + case REPEAT_MODE_ONE: + return STR_REPEAT_MODE_ONE; + case REPEAT_MODE_ALL: + return STR_REPEAT_MODE_ALL; + default: + throw new AssertionError("Illegal repeat mode: " + repeatMode); + } + } + } + + /** + * Enables and disables shuffle mode in the media player. + * + * @see Player#setShuffleModeEnabled(boolean) + */ + public static final class SetShuffleModeEnabled extends ExoCastMessage { + + /** The {@link Player#setShuffleModeEnabled(boolean) shuffleModeEnabled} value to set. */ + public boolean shuffleModeEnabled; + + /** @param shuffleModeEnabled See {@link #shuffleModeEnabled}. */ + public SetShuffleModeEnabled(boolean shuffleModeEnabled) { + super(METHOD_SET_SHUFFLE_MODE_ENABLED); + this.shuffleModeEnabled = shuffleModeEnabled; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject().put(KEY_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); + } + } + + /** See {@link Player#seekTo(int, long)}. */ + public static final class SeekTo extends ExoCastMessage { + + /** The {@link MediaItem#uuid} of the item to seek to. */ + public final UUID uuid; + /** + * The seek position in milliseconds in the specified item. If {@link C#TIME_UNSET}, the target + * position is the item's default position. + */ + public final long positionMs; + + /** + * @param uuid See {@link #uuid}. + * @param positionMs See {@link #positionMs}. + */ + public SeekTo(UUID uuid, long positionMs) { + super(METHOD_SEEK_TO); + this.uuid = uuid; + this.positionMs = positionMs; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + JSONObject result = new JSONObject().put(KEY_UUID, uuid); + ExoCastMessage.maybePutValue(result, KEY_POSITION_MS, positionMs, C.TIME_UNSET); + return result; + } + } + + /** See {@link Player#setPlaybackParameters(PlaybackParameters)}. */ + public static final class SetPlaybackParameters extends ExoCastMessage { + + /** The {@link Player#setPlaybackParameters(PlaybackParameters) parameters} to set. */ + public final PlaybackParameters playbackParameters; + + /** @param playbackParameters See {@link #playbackParameters}. */ + public SetPlaybackParameters(PlaybackParameters playbackParameters) { + super(METHOD_SET_PLAYBACK_PARAMETERS); + this.playbackParameters = playbackParameters; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + return new JSONObject() + .put(KEY_SPEED, playbackParameters.speed) + .put(KEY_PITCH, playbackParameters.pitch) + .put(KEY_SKIP_SILENCE, playbackParameters.skipSilence); + } + } + + /** See {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters)}. */ + public static final class SetTrackSelectionParameters extends ExoCastMessage { + + /** + * The {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters) parameters} to + * set + */ + public final TrackSelectionParameters trackSelectionParameters; + + public SetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { + super(METHOD_SET_TRACK_SELECTION_PARAMETERS); + this.trackSelectionParameters = trackSelectionParameters; + } + + @Override + protected JSONObject getArgumentsAsJsonObject() throws JSONException { + JSONArray disabledTextSelectionFlagsJson = new JSONArray(); + int disabledSelectionFlags = trackSelectionParameters.disabledTextTrackSelectionFlags; + if ((disabledSelectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) { + disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_AUTOSELECT); + } + if ((disabledSelectionFlags & C.SELECTION_FLAG_FORCED) != 0) { + disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_FORCED); + } + if ((disabledSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) { + disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_DEFAULT); + } + return new JSONObject() + .put(KEY_PREFERRED_AUDIO_LANGUAGE, trackSelectionParameters.preferredAudioLanguage) + .put(KEY_PREFERRED_TEXT_LANGUAGE, trackSelectionParameters.preferredTextLanguage) + .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, disabledTextSelectionFlagsJson) + .put( + KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, + trackSelectionParameters.selectUndeterminedTextLanguage); + } + } + + public final String method; + + /** + * Creates a message with the given method. + * + * @param method The method of the message. + */ + protected ExoCastMessage(String method) { + this.method = method; + } + + /** + * Returns a string containing a JSON representation of this message. + * + * @param sequenceNumber The sequence number to associate with this message. + * @return A string containing a JSON representation of this message. + */ + public final String toJsonString(long sequenceNumber) { + try { + JSONObject message = + new JSONObject() + .put(KEY_PROTOCOL_VERSION, PROTOCOL_VERSION) + .put(KEY_METHOD, method) + .put(KEY_SEQUENCE_NUMBER, sequenceNumber) + .put(KEY_ARGS, getArgumentsAsJsonObject()); + return message.toString(); + } catch (JSONException e) { + throw new AssertionError(e); + } + } + + /** Returns a {@link JSONObject} representation of the given item. */ + protected static JSONObject mediaItemAsJsonObject(MediaItem item) throws JSONException { + JSONObject itemAsJson = new JSONObject(); + itemAsJson.put(KEY_UUID, item.uuid); + itemAsJson.put(KEY_TITLE, item.title); + itemAsJson.put(KEY_DESCRIPTION, item.description); + itemAsJson.put(KEY_MEDIA, uriBundleAsJsonObject(item.media)); + // TODO(Internal b/118431961): Add attachment management. + + JSONArray drmSchemesAsJson = new JSONArray(); + for (MediaItem.DrmScheme drmScheme : item.drmSchemes) { + JSONObject drmSchemeAsJson = new JSONObject(); + drmSchemeAsJson.put(KEY_UUID, drmScheme.uuid); + if (drmScheme.licenseServer != null) { + drmSchemeAsJson.put(KEY_LICENSE_SERVER, uriBundleAsJsonObject(drmScheme.licenseServer)); + } + drmSchemesAsJson.put(drmSchemeAsJson); + } + itemAsJson.put(KEY_DRM_SCHEMES, drmSchemesAsJson); + maybePutValue(itemAsJson, KEY_START_POSITION_US, item.startPositionUs, C.TIME_UNSET); + maybePutValue(itemAsJson, KEY_END_POSITION_US, item.endPositionUs, C.TIME_UNSET); + itemAsJson.put(KEY_MIME_TYPE, item.mimeType); + return itemAsJson; + } + + /** Returns a {@link JSONObject JSON object} containing the arguments of the message. */ + protected abstract JSONObject getArgumentsAsJsonObject() throws JSONException; + + /** Returns a JSON representation of the given {@link UriBundle}. */ + protected static JSONObject uriBundleAsJsonObject(UriBundle uriBundle) throws JSONException { + return new JSONObject() + .put(KEY_URI, uriBundle.uri) + .put(KEY_REQUEST_HEADERS, new JSONObject(uriBundle.requestHeaders)); + } + + private static JSONArray getShuffleOrderAsJson(ShuffleOrder shuffleOrder) { + JSONArray shuffleOrderJson = new JSONArray(); + int index = shuffleOrder.getFirstIndex(); + while (index != C.INDEX_UNSET) { + shuffleOrderJson.put(index); + index = shuffleOrder.getNextIndex(index); + } + return shuffleOrderJson; + } + + private static void maybePutValue(JSONObject target, String key, long value, long unsetValue) + throws JSONException { + if (value != unsetValue) { + target.put(key, value); + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java new file mode 100644 index 0000000000..56b5d3cc8c --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java @@ -0,0 +1,40 @@ +/* + * 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; + +import android.content.Context; +import androidx.annotation.Nullable; +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.List; + +/** Cast options provider to target ExoPlayer's custom receiver app. */ +public final class ExoCastOptionsProvider implements OptionsProvider { + + public static final String RECEIVER_ID = "365DCC88"; + + @Override + public CastOptions getCastOptions(Context context) { + return new CastOptions.Builder().setReceiverApplicationId(RECEIVER_ID).build(); + } + + @Override + @Nullable + public List getAdditionalSessionProviders(Context context) { + return null; + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java new file mode 100644 index 0000000000..e24970ba0d --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java @@ -0,0 +1,958 @@ +/* + * 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; + +import android.os.Looper; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.BasePlayer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.IllegalSeekPositionException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.AddItems; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.MoveItem; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.RemoveItems; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetRepeatMode; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetShuffleModeEnabled; +import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetTrackSelectionParameters; +import com.google.android.exoplayer2.ext.cast.ExoCastTimeline.PeriodUid; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * Plays media in a Cast receiver app that implements the ExoCast message protocol. + * + *

      The ExoCast communication protocol consists in exchanging serialized {@link ExoCastMessage + * ExoCastMessages} and {@link ReceiverAppStateUpdate receiver app state updates}. + * + *

      All methods in this class must be invoked on the main thread. Operations that change the state + * of the receiver app are masked locally as if their effect was immediate in the receiver app. + * + *

      Methods that change the state of the player must only be invoked when a session is available, + * according to {@link CastSessionManager#isCastSessionAvailable()}. + */ +public final class ExoCastPlayer extends BasePlayer { + + private static final String TAG = "ExoCastPlayer"; + + private static final int RENDERER_COUNT = 4; + private static final int RENDERER_INDEX_VIDEO = 0; + private static final int RENDERER_INDEX_AUDIO = 1; + private static final int RENDERER_INDEX_TEXT = 2; + private static final int RENDERER_INDEX_METADATA = 3; + + private final Clock clock; + private final CastSessionManager castSessionManager; + private final CopyOnWriteArrayList listeners; + private final ArrayList notificationsBatch; + private final ArrayDeque ongoingNotificationsTasks; + private final Timeline.Period scratchPeriod; + @Nullable private SessionAvailabilityListener sessionAvailabilityListener; + + // Player state. + + private final List mediaItems; + private final StateHolder currentTimeline; + private ShuffleOrder currentShuffleOrder; + + private final StateHolder playbackState; + private final StateHolder playWhenReady; + private final StateHolder repeatMode; + private final StateHolder shuffleModeEnabled; + private final StateHolder isLoading; + private final StateHolder playbackParameters; + private final StateHolder trackselectionParameters; + private final StateHolder currentTrackGroups; + private final StateHolder currentTrackSelections; + private final StateHolder<@NullableType Object> currentManifest; + private final StateHolder<@NullableType PeriodUid> currentPeriodUid; + private final StateHolder playbackPositionMs; + private final HashMap currentMediaItemInfoMap; + private long lastPlaybackPositionChangeTimeMs; + @Nullable private ExoPlaybackException playbackError; + + /** + * Creates an instance using the system clock for calculating time deltas. + * + * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. + */ + public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory) { + this(castSessionManagerFactory, Clock.DEFAULT); + } + + /** + * Creates an instance using a custom {@link Clock} implementation. + * + * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. + * @param clock The clock to use for time delta calculations. + */ + public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory, Clock clock) { + this.clock = clock; + castSessionManager = castSessionManagerFactory.create(new SessionManagerStateListener()); + listeners = new CopyOnWriteArrayList<>(); + notificationsBatch = new ArrayList<>(); + ongoingNotificationsTasks = new ArrayDeque<>(); + scratchPeriod = new Timeline.Period(); + mediaItems = new ArrayList<>(); + currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ mediaItems.size()); + playbackState = new StateHolder<>(STATE_IDLE); + playWhenReady = new StateHolder<>(false); + repeatMode = new StateHolder<>(REPEAT_MODE_OFF); + shuffleModeEnabled = new StateHolder<>(false); + isLoading = new StateHolder<>(false); + playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); + trackselectionParameters = new StateHolder<>(TrackSelectionParameters.DEFAULT); + currentTrackGroups = new StateHolder<>(TrackGroupArray.EMPTY); + currentTrackSelections = new StateHolder<>(new TrackSelectionArray(null, null, null, null)); + currentManifest = new StateHolder<>(null); + currentTimeline = new StateHolder<>(ExoCastTimeline.EMPTY); + playbackPositionMs = new StateHolder<>(0L); + currentPeriodUid = new StateHolder<>(null); + currentMediaItemInfoMap = new HashMap<>(); + castSessionManager.start(); + } + + /** Returns whether a Cast session is available. */ + public boolean isCastSessionAvailable() { + return castSessionManager.isCastSessionAvailable(); + } + + /** + * Sets a listener for updates on the Cast session availability. + * + * @param listener The {@link SessionAvailabilityListener}. + */ + public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { + sessionAvailabilityListener = listener; + } + + /** + * Prepares the player for playback. + * + *

      Sends a preparation message to the receiver. If the player is in {@link #STATE_IDLE}, + * updates the timeline with the media queue contents. + */ + public void prepare() { + long sequence = castSessionManager.send(new ExoCastMessage.Prepare()); + if (playbackState.value == STATE_IDLE) { + playbackState.sequence = sequence; + setPlaybackStateInternal(mediaItems.isEmpty() ? STATE_ENDED : STATE_BUFFERING); + if (!currentTimeline.value.representsMediaQueue( + mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { + updateTimelineInternal(TIMELINE_CHANGE_REASON_PREPARED); + } + } + flushNotifications(); + } + + /** + * Returns the item at the given index. + * + * @param index The index of the item to retrieve. + * @return The item at the given index. + */ + public MediaItem getQueueItem(int index) { + return mediaItems.get(index); + } + + /** + * Equivalent to {@link #addItemsToQueue(int, MediaItem...) addItemsToQueue(C.INDEX_UNSET, + * items)}. + */ + public void addItemsToQueue(MediaItem... items) { + addItemsToQueue(C.INDEX_UNSET, 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. + * + *

      This method discards {@code items} with a uuid that already appears in the media queue. This + * method does nothing if {@code items} contains no new items. + * + * @param optionalIndex The index at which {@code items} will be inserted. If {@link + * C#INDEX_UNSET} is passed, the items are appended to the media queue. + * @param items The sequence of items to append. {@code items} must not contain items with + * matching uuids. + * @throws IllegalArgumentException If two or more elements in {@code items} contain matching + * uuids. + */ + public void addItemsToQueue(int optionalIndex, MediaItem... items) { + // Filter out items whose uuid already appears in the queue. + ArrayList itemsToAdd = new ArrayList<>(); + HashSet addedUuids = new HashSet<>(); + for (MediaItem item : items) { + Assertions.checkArgument( + addedUuids.add(item.uuid), "Added items must contain distinct uuids"); + if (playbackState.value == STATE_IDLE + || currentTimeline.value.getWindowIndexFromUuid(item.uuid) == C.INDEX_UNSET) { + // Prevent adding items that exist in the timeline. If the player is not yet prepared, + // ignore this check, since the timeline may not reflect the current media queue. + // Preparation will filter any duplicates. + itemsToAdd.add(item); + } + } + if (itemsToAdd.isEmpty()) { + return; + } + + int normalizedIndex; + if (optionalIndex != C.INDEX_UNSET) { + normalizedIndex = optionalIndex; + mediaItems.addAll(optionalIndex, itemsToAdd); + } else { + normalizedIndex = mediaItems.size(); + mediaItems.addAll(itemsToAdd); + } + currentShuffleOrder = currentShuffleOrder.cloneAndInsert(normalizedIndex, itemsToAdd.size()); + long sequence = + castSessionManager.send(new AddItems(optionalIndex, itemsToAdd, currentShuffleOrder)); + if (playbackState.value != STATE_IDLE) { + currentTimeline.sequence = sequence; + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + } + flushNotifications(); + } + + /** + * Moves an existing item within the queue. + * + *

      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 index The index of the item to move. + * @param newIndex The index at which the item will be placed after this operation. + */ + public void moveItemInQueue(int index, int newIndex) { + MediaItem movedItem = mediaItems.remove(index); + mediaItems.add(newIndex, movedItem); + currentShuffleOrder = + currentShuffleOrder + .cloneAndRemove(index, index + 1) + .cloneAndInsert(newIndex, /* insertionCount= */ 1); + long sequence = + castSessionManager.send(new MoveItem(movedItem.uuid, newIndex, currentShuffleOrder)); + if (playbackState.value != STATE_IDLE) { + currentTimeline.sequence = sequence; + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + } + flushNotifications(); + } + + /** + * Removes an item from the queue. + * + * @param index The index of the item to remove from the queue. + */ + public void removeItemFromQueue(int index) { + removeRangeFromQueue(index, index + 1); + } + + /** + * Removes a range of items from the queue. + * + *

      If the currently-playing item is removed, the playback position moves to the item following + * the removed range. If no item follows the removed range, the position is set to the last item + * in the queue and the player state transitions to {@link #STATE_ENDED}. Does nothing if an empty + * range ({@code from == exclusiveTo}) is passed. + * + * @param indexFrom The inclusive index at which the range to remove starts. + * @param indexExclusiveTo The exclusive index at which the range to remove ends. + */ + public void removeRangeFromQueue(int indexFrom, int indexExclusiveTo) { + UUID[] uuidsToRemove = new UUID[indexExclusiveTo - indexFrom]; + for (int i = 0; i < uuidsToRemove.length; i++) { + uuidsToRemove[i] = mediaItems.get(i + indexFrom).uuid; + } + + int windowIndexBeforeRemoval = getCurrentWindowIndex(); + boolean currentItemWasRemoved = + windowIndexBeforeRemoval >= indexFrom && windowIndexBeforeRemoval < indexExclusiveTo; + boolean shouldTransitionToEnded = + currentItemWasRemoved && indexExclusiveTo == mediaItems.size(); + + Util.removeRange(mediaItems, indexFrom, indexExclusiveTo); + long sequence = castSessionManager.send(new RemoveItems(Arrays.asList(uuidsToRemove))); + currentShuffleOrder = currentShuffleOrder.cloneAndRemove(indexFrom, indexExclusiveTo); + + if (playbackState.value != STATE_IDLE) { + currentTimeline.sequence = sequence; + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + if (currentItemWasRemoved) { + int newWindowIndex = Math.max(0, indexFrom - (shouldTransitionToEnded ? 1 : 0)); + PeriodUid periodUid = + currentTimeline.value.isEmpty() + ? null + : (PeriodUid) + currentTimeline.value.getPeriodPosition( + window, + scratchPeriod, + newWindowIndex, + /* windowPositionUs= */ C.TIME_UNSET) + .first; + currentPeriodUid.sequence = sequence; + playbackPositionMs.sequence = sequence; + setPlaybackPositionInternal( + periodUid, + /* positionMs= */ C.TIME_UNSET, + /* discontinuityReason= */ DISCONTINUITY_REASON_SEEK); + } + playbackState.sequence = sequence; + setPlaybackStateInternal(shouldTransitionToEnded ? STATE_ENDED : STATE_BUFFERING); + } + flushNotifications(); + } + + /** Removes all items in the queue. */ + public void clearQueue() { + removeRangeFromQueue(0, getQueueSize()); + } + + /** Returns the number of items in this queue. */ + public int getQueueSize() { + return mediaItems.size(); + } + + // Track selection. + + /** + * Provides a set of constrains for the receiver app to execute track selection. + * + *

      {@link TrackSelectionParameters} passed to this method may be {@link + * TrackSelectionParameters#buildUpon() built upon} by this player as a result of a remote + * operation, which means {@link TrackSelectionParameters} obtained from {@link + * #getTrackSelectionParameters()} may have field differences with {@code parameters} passed to + * this method. However, only fields modified remotely will present differences. Other fields will + * remain unchanged. + */ + public void setTrackSelectionParameters(TrackSelectionParameters trackselectionParameters) { + this.trackselectionParameters.value = trackselectionParameters; + this.trackselectionParameters.sequence = + castSessionManager.send(new SetTrackSelectionParameters(trackselectionParameters)); + } + + /** + * Retrieves the current {@link TrackSelectionParameters}. See {@link + * #setTrackSelectionParameters(TrackSelectionParameters)}. + */ + public TrackSelectionParameters getTrackSelectionParameters() { + return trackselectionParameters.value; + } + + // Player Implementation. + + @Override + @Nullable + public AudioComponent getAudioComponent() { + // TODO: Implement volume controls using the audio component. + return null; + } + + @Override + @Nullable + public VideoComponent getVideoComponent() { + return null; + } + + @Override + @Nullable + public TextComponent getTextComponent() { + return null; + } + + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return null; + } + + @Override + public Looper getApplicationLooper() { + return Looper.getMainLooper(); + } + + @Override + public void addListener(EventListener listener) { + listeners.addIfAbsent(new ListenerHolder(listener)); + } + + @Override + public void removeListener(EventListener listener) { + for (ListenerHolder listenerHolder : listeners) { + if (listenerHolder.listener.equals(listener)) { + listenerHolder.release(); + listeners.remove(listenerHolder); + } + } + } + + @Override + @Player.State + public int getPlaybackState() { + return playbackState.value; + } + + @Nullable + @Override + public ExoPlaybackException getPlaybackError() { + return playbackError; + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + this.playWhenReady.sequence = + castSessionManager.send(new ExoCastMessage.SetPlayWhenReady(playWhenReady)); + // Take a snapshot of the playback position before pausing to ensure future calculations are + // correct. + setPlaybackPositionInternal( + currentPeriodUid.value, getCurrentPosition(), /* discontinuityReason= */ null); + setPlayWhenReadyInternal(playWhenReady); + flushNotifications(); + } + + @Override + public boolean getPlayWhenReady() { + return playWhenReady.value; + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + this.repeatMode.sequence = castSessionManager.send(new SetRepeatMode(repeatMode)); + setRepeatModeInternal(repeatMode); + flushNotifications(); + } + + @Override + @RepeatMode + public int getRepeatMode() { + return repeatMode.value; + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + this.shuffleModeEnabled.sequence = + castSessionManager.send(new SetShuffleModeEnabled(shuffleModeEnabled)); + setShuffleModeEnabledInternal(shuffleModeEnabled); + flushNotifications(); + } + + @Override + public boolean getShuffleModeEnabled() { + return shuffleModeEnabled.value; + } + + @Override + public boolean isLoading() { + return isLoading.value; + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + if (mediaItems.isEmpty()) { + // TODO: Handle seeking in empty timeline. + setPlaybackPositionInternal(/* periodUid= */ null, 0, DISCONTINUITY_REASON_SEEK); + return; + } else if (windowIndex >= mediaItems.size()) { + throw new IllegalSeekPositionException(currentTimeline.value, windowIndex, positionMs); + } + long sequence = + castSessionManager.send( + new ExoCastMessage.SeekTo(mediaItems.get(windowIndex).uuid, positionMs)); + + currentPeriodUid.sequence = sequence; + playbackPositionMs.sequence = sequence; + + PeriodUid periodUid = + (PeriodUid) + currentTimeline.value.getPeriodPosition( + window, scratchPeriod, windowIndex, C.msToUs(positionMs)) + .first; + setPlaybackPositionInternal(periodUid, positionMs, DISCONTINUITY_REASON_SEEK); + if (playbackState.value != STATE_IDLE) { + playbackState.sequence = sequence; + setPlaybackStateInternal(STATE_BUFFERING); + } + flushNotifications(); + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + playbackParameters = + playbackParameters != null ? playbackParameters : PlaybackParameters.DEFAULT; + this.playbackParameters.value = playbackParameters; + this.playbackParameters.sequence = + castSessionManager.send(new ExoCastMessage.SetPlaybackParameters(playbackParameters)); + this.playbackParameters.value = playbackParameters; + // Note: This method, unlike others, does not immediately notify the change. See the Player + // interface for more information. + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters.value; + } + + @Override + public void stop(boolean reset) { + long sequence = castSessionManager.send(new ExoCastMessage.Stop(reset)); + playbackState.sequence = sequence; + setPlaybackStateInternal(STATE_IDLE); + if (reset) { + currentTimeline.sequence = sequence; + mediaItems.clear(); + currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length =*/ 0); + setPlaybackPositionInternal( + /* periodUid= */ null, /* positionMs= */ 0, DISCONTINUITY_REASON_INTERNAL); + updateTimelineInternal(TIMELINE_CHANGE_REASON_RESET); + } + flushNotifications(); + } + + @Override + public void release() { + setSessionAvailabilityListener(null); + castSessionManager.stopTrackingSession(); + flushNotifications(); + } + + @Override + public int getRendererCount() { + return RENDERER_COUNT; + } + + @Override + public int getRendererType(int index) { + switch (index) { + case RENDERER_INDEX_VIDEO: + return C.TRACK_TYPE_VIDEO; + case RENDERER_INDEX_AUDIO: + return C.TRACK_TYPE_AUDIO; + case RENDERER_INDEX_TEXT: + return C.TRACK_TYPE_TEXT; + case RENDERER_INDEX_METADATA: + return C.TRACK_TYPE_METADATA; + default: + throw new IndexOutOfBoundsException(); + } + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. + return currentTrackGroups.value; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. + return currentTrackSelections.value; + } + + @Override + @Nullable + public Object getCurrentManifest() { + // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. + return currentManifest.value; + } + + @Override + public Timeline getCurrentTimeline() { + return currentTimeline.value; + } + + @Override + public int getCurrentPeriodIndex() { + int periodIndex = + currentPeriodUid.value == null + ? C.INDEX_UNSET + : currentTimeline.value.getIndexOfPeriod(currentPeriodUid.value); + return periodIndex != C.INDEX_UNSET ? periodIndex : 0; + } + + @Override + public int getCurrentWindowIndex() { + int windowIndex = + currentPeriodUid.value == null + ? C.INDEX_UNSET + : currentTimeline.value.getWindowIndexContainingPeriod(currentPeriodUid.value); + return windowIndex != C.INDEX_UNSET ? windowIndex : 0; + } + + @Override + public long getDuration() { + return getContentDuration(); + } + + @Override + public long getCurrentPosition() { + return playbackPositionMs.value + + (getPlaybackState() == STATE_READY && getPlayWhenReady() + ? projectPlaybackTimeElapsedMs() + : 0L); + } + + @Override + public long getBufferedPosition() { + return getCurrentPosition(); + } + + @Override + public long getTotalBufferedDuration() { + return 0; + } + + @Override + public boolean isPlayingAd() { + // TODO (Internal b/119293631): Add support for ads. + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return C.INDEX_UNSET; + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + @Override + public long getContentBufferedPosition() { + return getCurrentPosition(); + } + + // Local state modifications. + + private void setPlayWhenReadyInternal(boolean playWhenReady) { + if (this.playWhenReady.value != playWhenReady) { + this.playWhenReady.value = playWhenReady; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(playWhenReady, playbackState.value))); + } + } + + private void setPlaybackStateInternal(int playbackState) { + if (this.playbackState.value != playbackState) { + if (this.playbackState.value == STATE_IDLE) { + // We are transitioning out of STATE_IDLE. We clear any errors. + setPlaybackErrorInternal(null); + } + this.playbackState.value = playbackState; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(playWhenReady.value, playbackState))); + } + } + + private void setRepeatModeInternal(int repeatMode) { + if (this.repeatMode.value != repeatMode) { + this.repeatMode.value = repeatMode; + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode))); + } + } + + private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) { + if (this.shuffleModeEnabled.value != shuffleModeEnabled) { + this.shuffleModeEnabled.value = shuffleModeEnabled; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled))); + } + } + + private void setIsLoadingInternal(boolean isLoading) { + if (this.isLoading.value != isLoading) { + this.isLoading.value = isLoading; + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onLoadingChanged(isLoading))); + } + } + + private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { + if (!this.playbackParameters.value.equals(playbackParameters)) { + this.playbackParameters.value = playbackParameters; + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlaybackParametersChanged(playbackParameters))); + } + } + + private void setPlaybackErrorInternal(@Nullable String errorMessage) { + if (errorMessage != null) { + playbackError = ExoPlaybackException.createForRemote(errorMessage); + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerError(Assertions.checkNotNull(playbackError)))); + } else { + playbackError = null; + } + } + + private void setPlaybackPositionInternal( + @Nullable PeriodUid periodUid, long positionMs, @Nullable Integer discontinuityReason) { + currentPeriodUid.value = periodUid; + if (periodUid == null) { + positionMs = 0L; + } else if (positionMs == C.TIME_UNSET) { + int windowIndex = currentTimeline.value.getWindowIndexContainingPeriod(periodUid); + if (windowIndex == C.INDEX_UNSET) { + positionMs = 0; + } else { + positionMs = + C.usToMs( + currentTimeline.value.getWindow(windowIndex, window, /* setTag= */ false) + .defaultPositionUs); + } + } + playbackPositionMs.value = positionMs; + lastPlaybackPositionChangeTimeMs = clock.elapsedRealtime(); + if (discontinuityReason != null) { + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPositionDiscontinuity(discontinuityReason))); + } + } + + // Internal methods. + + private void updateTimelineInternal(@TimelineChangeReason int changeReason) { + currentTimeline.value = + ExoCastTimeline.createTimelineFor(mediaItems, currentMediaItemInfoMap, currentShuffleOrder); + removeStaleMediaItemInfo(); + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onTimelineChanged( + currentTimeline.value, /* manifest= */ null, changeReason))); + } + + private long projectPlaybackTimeElapsedMs() { + return (long) + ((clock.elapsedRealtime() - lastPlaybackPositionChangeTimeMs) + * playbackParameters.value.speed); + } + + 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(); + } + } + + /** + * Updates the current media item information by including any extra entries received from the + * receiver app. + * + * @param mediaItemsInformation A map of media item information received from the receiver app. + */ + private void updateMediaItemsInfo(Map mediaItemsInformation) { + for (Map.Entry entry : mediaItemsInformation.entrySet()) { + MediaItemInfo currentInfoForEntry = currentMediaItemInfoMap.get(entry.getKey()); + boolean shouldPutEntry = + currentInfoForEntry == null || !currentInfoForEntry.equals(entry.getValue()); + if (shouldPutEntry) { + currentMediaItemInfoMap.put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Removes stale media info entries. An entry is considered stale when the corresponding media + * item is not present in the current media queue. + */ + private void removeStaleMediaItemInfo() { + for (Iterator iterator = currentMediaItemInfoMap.keySet().iterator(); + iterator.hasNext(); ) { + UUID uuid = iterator.next(); + if (currentTimeline.value.getWindowIndexFromUuid(uuid) == C.INDEX_UNSET) { + iterator.remove(); + } + } + } + + // Internal classes. + + private class SessionManagerStateListener implements CastSessionManager.StateListener { + + @Override + public void onCastSessionAvailable() { + if (sessionAvailabilityListener != null) { + sessionAvailabilityListener.onCastSessionAvailable(); + } + } + + @Override + public void onCastSessionUnavailable() { + if (sessionAvailabilityListener != null) { + sessionAvailabilityListener.onCastSessionUnavailable(); + } + } + + @Override + public void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate) { + long sequence = stateUpdate.sequenceNumber; + + if (stateUpdate.errorMessage != null) { + setPlaybackErrorInternal(stateUpdate.errorMessage); + } + + if (sequence >= playbackState.sequence && stateUpdate.playbackState != null) { + setPlaybackStateInternal(stateUpdate.playbackState); + } + + if (sequence >= currentTimeline.sequence) { + if (stateUpdate.items != null) { + mediaItems.clear(); + mediaItems.addAll(stateUpdate.items); + } + + currentShuffleOrder = + stateUpdate.shuffleOrder != null + ? new ShuffleOrder.DefaultShuffleOrder( + Util.toArray(stateUpdate.shuffleOrder), clock.elapsedRealtime()) + : currentShuffleOrder; + updateMediaItemsInfo(stateUpdate.mediaItemsInformation); + + if (playbackState.value != STATE_IDLE + && !currentTimeline.value.representsMediaQueue( + mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { + updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); + } + } + + if (sequence >= currentPeriodUid.sequence + && stateUpdate.currentPlayingItemUuid != null + && stateUpdate.currentPlaybackPositionMs != null) { + PeriodUid periodUid; + if (stateUpdate.currentPlayingPeriodId == null) { + int windowIndex = + currentTimeline.value.getWindowIndexFromUuid(stateUpdate.currentPlayingItemUuid); + periodUid = + (PeriodUid) + currentTimeline.value.getPeriodPosition( + window, + scratchPeriod, + windowIndex, + C.msToUs(stateUpdate.currentPlaybackPositionMs)) + .first; + } else { + periodUid = + ExoCastTimeline.createPeriodUid( + stateUpdate.currentPlayingItemUuid, stateUpdate.currentPlayingPeriodId); + } + setPlaybackPositionInternal( + periodUid, stateUpdate.currentPlaybackPositionMs, stateUpdate.discontinuityReason); + } + + if (sequence >= isLoading.sequence && stateUpdate.isLoading != null) { + setIsLoadingInternal(stateUpdate.isLoading); + } + + if (sequence >= playWhenReady.sequence && stateUpdate.playWhenReady != null) { + setPlayWhenReadyInternal(stateUpdate.playWhenReady); + } + + if (sequence >= shuffleModeEnabled.sequence && stateUpdate.shuffleModeEnabled != null) { + setShuffleModeEnabledInternal(stateUpdate.shuffleModeEnabled); + } + + if (sequence >= repeatMode.sequence && stateUpdate.repeatMode != null) { + setRepeatModeInternal(stateUpdate.repeatMode); + } + + if (sequence >= playbackParameters.sequence && stateUpdate.playbackParameters != null) { + setPlaybackParametersInternal(stateUpdate.playbackParameters); + } + + TrackSelectionParameters parameters = stateUpdate.trackSelectionParameters; + if (sequence >= trackselectionParameters.sequence && parameters != null) { + trackselectionParameters.value = + trackselectionParameters + .value + .buildUpon() + .setDisabledTextTrackSelectionFlags(parameters.disabledTextTrackSelectionFlags) + .setPreferredAudioLanguage(parameters.preferredAudioLanguage) + .setPreferredTextLanguage(parameters.preferredTextLanguage) + .setSelectUndeterminedTextLanguage(parameters.selectUndeterminedTextLanguage) + .build(); + } + + flushNotifications(); + } + } + + private static final class StateHolder { + + public T value; + public long sequence; + + public StateHolder(T initialValue) { + value = initialValue; + sequence = CastSessionManager.SEQUENCE_NUMBER_UNSET; + } + } + + 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/ExoCastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java new file mode 100644 index 0000000000..115536ac4c --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java @@ -0,0 +1,342 @@ +/* + * 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; + +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.source.ShuffleOrder; +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; + +/** + * A {@link Timeline} for Cast receiver app media queues. + * + *

      Each {@link MediaItem} in the timeline is exposed as a window. Unprepared media items are + * exposed as an unset-duration {@link Window}, with a single unset-duration {@link Period}. + */ +/* package */ final class ExoCastTimeline extends Timeline { + + /** Opaque object that uniquely identifies a period across timeline changes. */ + public interface PeriodUid {} + + /** A timeline for an empty media queue. */ + public static final ExoCastTimeline EMPTY = + createTimelineFor( + Collections.emptyList(), Collections.emptyMap(), new ShuffleOrder.DefaultShuffleOrder(0)); + + /** + * Creates {@link PeriodUid} from the given arguments. + * + * @param itemUuid The UUID that identifies the item. + * @param periodId The id of the period for which the unique identifier is required. + * @return An opaque unique identifier for a period. + */ + public static PeriodUid createPeriodUid(UUID itemUuid, Object periodId) { + return new PeriodUidImpl(itemUuid, periodId); + } + + /** + * Returns a new timeline representing the given media queue information. + * + * @param mediaItems The media items conforming the timeline. + * @param mediaItemInfoMap Maps {@link MediaItem media items} in {@code mediaItems} to a {@link + * MediaItemInfo} through their {@link MediaItem#uuid}. Media items may not have a {@link + * MediaItemInfo} mapped to them. + * @param shuffleOrder The {@link ShuffleOrder} of the timeline. {@link ShuffleOrder#getLength()} + * must be equal to {@code mediaItems.size()}. + * @return A new timeline representing the given media queue information. + */ + public static ExoCastTimeline createTimelineFor( + List mediaItems, + Map mediaItemInfoMap, + ShuffleOrder shuffleOrder) { + Assertions.checkArgument(mediaItems.size() == shuffleOrder.getLength()); + int[] accumulativePeriodCount = new int[mediaItems.size()]; + int periodCount = 0; + for (int i = 0; i < accumulativePeriodCount.length; i++) { + periodCount += getInfoOrEmpty(mediaItemInfoMap, mediaItems.get(i).uuid).periods.size(); + accumulativePeriodCount[i] = periodCount; + } + HashMap uuidToIndex = new HashMap<>(); + for (int i = 0; i < mediaItems.size(); i++) { + uuidToIndex.put(mediaItems.get(i).uuid, i); + } + return new ExoCastTimeline( + Collections.unmodifiableList(new ArrayList<>(mediaItems)), + Collections.unmodifiableMap(new HashMap<>(mediaItemInfoMap)), + Collections.unmodifiableMap(new HashMap<>(uuidToIndex)), + shuffleOrder, + accumulativePeriodCount); + } + + // Timeline backing information. + private final List mediaItems; + private final Map mediaItemInfoMap; + private final ShuffleOrder shuffleOrder; + + // Precomputed for quick access. + private final Map uuidToIndex; + private final int[] accumulativePeriodCount; + + private ExoCastTimeline( + List mediaItems, + Map mediaItemInfoMap, + Map uuidToIndex, + ShuffleOrder shuffleOrder, + int[] accumulativePeriodCount) { + this.mediaItems = mediaItems; + this.mediaItemInfoMap = mediaItemInfoMap; + this.uuidToIndex = uuidToIndex; + this.shuffleOrder = shuffleOrder; + this.accumulativePeriodCount = accumulativePeriodCount; + } + + /** + * Returns whether the given media queue information would produce a timeline equivalent to this + * one. + * + * @see ExoCastTimeline#createTimelineFor(List, Map, ShuffleOrder) + */ + public boolean representsMediaQueue( + List mediaItems, + Map mediaItemInfoMap, + ShuffleOrder shuffleOrder) { + if (this.shuffleOrder.getLength() != shuffleOrder.getLength()) { + return false; + } + + int index = shuffleOrder.getFirstIndex(); + if (this.shuffleOrder.getFirstIndex() != index) { + return false; + } + while (index != C.INDEX_UNSET) { + int nextIndex = shuffleOrder.getNextIndex(index); + if (nextIndex != this.shuffleOrder.getNextIndex(index)) { + return false; + } + index = nextIndex; + } + + if (mediaItems.size() != this.mediaItems.size()) { + return false; + } + for (int i = 0; i < mediaItems.size(); i++) { + UUID uuid = mediaItems.get(i).uuid; + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); + if (!uuid.equals(this.mediaItems.get(i).uuid) + || !mediaItemInfo.equals(getInfoOrEmpty(this.mediaItemInfoMap, uuid))) { + return false; + } + } + return true; + } + + /** + * Returns the index of the window that contains the period identified by the given {@code + * periodUid} or {@link C#INDEX_UNSET} if this timeline does not contain any period with the given + * {@code periodUid}. + */ + public int getWindowIndexContainingPeriod(PeriodUid periodUid) { + if (!(periodUid instanceof PeriodUidImpl)) { + return C.INDEX_UNSET; + } + return getWindowIndexFromUuid(((PeriodUidImpl) periodUid).itemUuid); + } + + /** + * Returns the index of the window that represents the media item with the given {@code uuid} or + * {@link C#INDEX_UNSET} if no item in this timeline has the given {@code uuid}. + */ + public int getWindowIndexFromUuid(UUID uuid) { + Integer index = uuidToIndex.get(uuid); + return index != null ? index : C.INDEX_UNSET; + } + + // Timeline implementation. + + @Override + public int getWindowCount() { + return mediaItems.size(); + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + MediaItem mediaItem = mediaItems.get(windowIndex); + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, mediaItem.uuid); + return window.set( + /* tag= */ setTag ? mediaItem.attachment : null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ mediaItemInfo.isSeekable, + /* isDynamic= */ mediaItemInfo.isDynamic, + /* defaultPositionUs= */ mediaItemInfo.defaultStartPositionUs, + /* durationUs= */ mediaItemInfo.windowDurationUs, + /* firstPeriodIndex= */ windowIndex == 0 ? 0 : accumulativePeriodCount[windowIndex - 1], + /* lastPeriodIndex= */ accumulativePeriodCount[windowIndex] - 1, + mediaItemInfo.positionInFirstPeriodUs); + } + + @Override + public int getPeriodCount() { + return mediaItems.isEmpty() ? 0 : accumulativePeriodCount[accumulativePeriodCount.length - 1]; + } + + @Override + public Period getPeriodByUid(Object periodUidObject, Period period) { + return getPeriodInternal((PeriodUidImpl) periodUidObject, period, /* setIds= */ true); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return getPeriodInternal((PeriodUidImpl) getUidOfPeriod(periodIndex), period, setIds); + } + + @Override + public int getIndexOfPeriod(Object uid) { + if (!(uid instanceof PeriodUidImpl)) { + return C.INDEX_UNSET; + } + PeriodUidImpl periodUid = (PeriodUidImpl) uid; + UUID uuid = periodUid.itemUuid; + Integer itemIndex = uuidToIndex.get(uuid); + if (itemIndex == null) { + return C.INDEX_UNSET; + } + int indexOfPeriodInItem = + getInfoOrEmpty(mediaItemInfoMap, uuid).getIndexOfPeriod(periodUid.periodId); + if (indexOfPeriodInItem == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return indexOfPeriodInItem + (itemIndex == 0 ? 0 : accumulativePeriodCount[itemIndex - 1]); + } + + @Override + public PeriodUid getUidOfPeriod(int periodIndex) { + int mediaItemIndex = getMediaItemIndexForPeriodIndex(periodIndex); + int periodIndexInMediaItem = + periodIndex - (mediaItemIndex > 0 ? accumulativePeriodCount[mediaItemIndex - 1] : 0); + UUID uuid = mediaItems.get(mediaItemIndex).uuid; + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); + return new PeriodUidImpl(uuid, mediaItemInfo.periods.get(periodIndexInMediaItem).id); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getLastIndex() : mediaItems.size() - 1; + } + + @Override + public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } else if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_OFF + ? C.INDEX_UNSET + : getLastWindowIndex(shuffleModeEnabled); + } else if (shuffleModeEnabled) { + return shuffleOrder.getPreviousIndex(windowIndex); + } else { + return windowIndex - 1; + } + } + + @Override + public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { + if (repeatMode == Player.REPEAT_MODE_ONE) { + return windowIndex; + } else if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { + return repeatMode == Player.REPEAT_MODE_OFF + ? C.INDEX_UNSET + : getFirstWindowIndex(shuffleModeEnabled); + } else if (shuffleModeEnabled) { + return shuffleOrder.getNextIndex(windowIndex); + } else { + return windowIndex + 1; + } + } + + // Internal methods. + + private Period getPeriodInternal(PeriodUidImpl uid, Period period, boolean setIds) { + UUID uuid = uid.itemUuid; + int itemIndex = Assertions.checkNotNull(uuidToIndex.get(uuid)); + MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); + MediaItemInfo.Period mediaInfoPeriod = + mediaItemInfo.periods.get(mediaItemInfo.getIndexOfPeriod(uid.periodId)); + return period.set( + setIds ? mediaInfoPeriod.id : null, + setIds ? uid : null, + /* windowIndex= */ itemIndex, + mediaInfoPeriod.durationUs, + mediaInfoPeriod.positionInWindowUs); + } + + private int getMediaItemIndexForPeriodIndex(int periodIndex) { + return Util.binarySearchCeil( + accumulativePeriodCount, periodIndex, /* inclusive= */ false, /* stayInBounds= */ false); + } + + private static MediaItemInfo getInfoOrEmpty(Map map, UUID uuid) { + MediaItemInfo info = map.get(uuid); + return info != null ? info : MediaItemInfo.EMPTY; + } + + // Internal classes. + + private static final class PeriodUidImpl implements PeriodUid { + + public final UUID itemUuid; + public final Object periodId; + + private PeriodUidImpl(UUID itemUuid, Object periodId) { + this.itemUuid = itemUuid; + this.periodId = periodId; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + PeriodUidImpl periodUid = (PeriodUidImpl) other; + return itemUuid.equals(periodUid.itemUuid) && periodId.equals(periodUid.periodId); + } + + @Override + public int hashCode() { + int result = itemUuid.hashCode(); + result = 31 * result + periodId.hashCode(); + return result; + } + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java new file mode 100644 index 0000000000..cb5eff4f37 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java @@ -0,0 +1,160 @@ +/* + * 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; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.List; + +// TODO (Internal b/119293631): Add ad playback state info. +/** + * Holds dynamic information for a {@link MediaItem}. + * + *

      Holds information related to preparation for a specific {@link MediaItem}. Unprepared items + * are associated with an {@link #EMPTY} info object until prepared. + */ +public final class MediaItemInfo { + + /** Placeholder information for media items that have not yet been prepared by the player. */ + public static final MediaItemInfo EMPTY = + new MediaItemInfo( + /* windowDurationUs= */ C.TIME_UNSET, + /* defaultStartPositionUs= */ 0L, + Collections.singletonList( + new Period( + /* id= */ new Object(), + /* durationUs= */ C.TIME_UNSET, + /* positionInWindowUs= */ 0L)), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ false, + /* isDynamic= */ true); + + /** Holds the information of one of the periods of a {@link MediaItem}. */ + public static final class Period { + + /** + * The id of the period. Must be unique within the {@link MediaItem} but may match with periods + * in other items. + */ + public final Object id; + /** The duration of the period in microseconds. */ + public final long durationUs; + /** The position of this period in the window in microseconds. */ + public final long positionInWindowUs; + // TODO: Add track information. + + public Period(Object id, long durationUs, long positionInWindowUs) { + this.id = id; + this.durationUs = durationUs; + this.positionInWindowUs = positionInWindowUs; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + Period period = (Period) other; + return durationUs == period.durationUs + && positionInWindowUs == period.positionInWindowUs + && id.equals(period.id); + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); + return result; + } + } + + /** The duration of the window in microseconds. */ + public final long windowDurationUs; + /** The default start position relative to the start of the window, in microseconds. */ + public final long defaultStartPositionUs; + /** The periods conforming the media item. */ + public final List periods; + /** The position of the window in the first period in microseconds. */ + public final long positionInFirstPeriodUs; + /** Whether it is possible to seek within the window. */ + public final boolean isSeekable; + /** Whether the window may change when the timeline is updated. */ + public final boolean isDynamic; + + public MediaItemInfo( + long windowDurationUs, + long defaultStartPositionUs, + List periods, + long positionInFirstPeriodUs, + boolean isSeekable, + boolean isDynamic) { + this.windowDurationUs = windowDurationUs; + this.defaultStartPositionUs = defaultStartPositionUs; + this.periods = Collections.unmodifiableList(periods); + this.positionInFirstPeriodUs = positionInFirstPeriodUs; + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + } + + /** + * Returns the index of the period with {@link Period#id} equal to {@code periodId}, or {@link + * C#INDEX_UNSET} if none of the periods has the given id. + */ + public int getIndexOfPeriod(Object periodId) { + for (int i = 0; i < periods.size(); i++) { + if (Util.areEqual(periods.get(i).id, periodId)) { + return i; + } + } + return C.INDEX_UNSET; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + MediaItemInfo that = (MediaItemInfo) other; + return windowDurationUs == that.windowDurationUs + && defaultStartPositionUs == that.defaultStartPositionUs + && positionInFirstPeriodUs == that.positionInFirstPeriodUs + && isSeekable == that.isSeekable + && isDynamic == that.isDynamic + && periods.equals(that.periods); + } + + @Override + public int hashCode() { + int result = (int) (windowDurationUs ^ (windowDurationUs >>> 32)); + result = 31 * result + (int) (defaultStartPositionUs ^ (defaultStartPositionUs >>> 32)); + result = 31 * result + periods.hashCode(); + result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); + result = 31 * result + (isSeekable ? 1 : 0); + result = 31 * result + (isDynamic ? 1 : 0); + return result; + } +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java new file mode 100644 index 0000000000..8cb6056340 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java @@ -0,0 +1,633 @@ +/* + * 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.ext.cast; + +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.Player.STATE_BUFFERING; +import static com.google.android.exoplayer2.Player.STATE_ENDED; +import static com.google.android.exoplayer2.Player.STATE_IDLE; +import static com.google.android.exoplayer2.Player.STATE_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_ENDED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_IDLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_READY; + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** Holds a playback state update from the receiver app. */ +public final class ReceiverAppStateUpdate { + + /** Builder for {@link ReceiverAppStateUpdate}. */ + public static final class Builder { + + private final long sequenceNumber; + @MonotonicNonNull private Boolean playWhenReady; + @MonotonicNonNull private Integer playbackState; + @MonotonicNonNull private List items; + @MonotonicNonNull private Integer repeatMode; + @MonotonicNonNull private Boolean shuffleModeEnabled; + @MonotonicNonNull private Boolean isLoading; + @MonotonicNonNull private PlaybackParameters playbackParameters; + @MonotonicNonNull private TrackSelectionParameters trackSelectionParameters; + @MonotonicNonNull private String errorMessage; + @MonotonicNonNull private Integer discontinuityReason; + @MonotonicNonNull private UUID currentPlayingItemUuid; + @MonotonicNonNull private String currentPlayingPeriodId; + @MonotonicNonNull private Long currentPlaybackPositionMs; + @MonotonicNonNull private List shuffleOrder; + private Map mediaItemsInformation; + + private Builder(long sequenceNumber) { + this.sequenceNumber = sequenceNumber; + mediaItemsInformation = Collections.emptyMap(); + } + + /** See {@link ReceiverAppStateUpdate#playWhenReady}. */ + public Builder setPlayWhenReady(Boolean playWhenReady) { + this.playWhenReady = playWhenReady; + return this; + } + + /** See {@link ReceiverAppStateUpdate#playbackState}. */ + public Builder setPlaybackState(Integer playbackState) { + this.playbackState = playbackState; + return this; + } + + /** See {@link ReceiverAppStateUpdate#items}. */ + public Builder setItems(List items) { + this.items = Collections.unmodifiableList(items); + return this; + } + + /** See {@link ReceiverAppStateUpdate#repeatMode}. */ + public Builder setRepeatMode(Integer repeatMode) { + this.repeatMode = repeatMode; + return this; + } + + /** See {@link ReceiverAppStateUpdate#shuffleModeEnabled}. */ + public Builder setShuffleModeEnabled(Boolean shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + return this; + } + + /** See {@link ReceiverAppStateUpdate#isLoading}. */ + public Builder setIsLoading(Boolean isLoading) { + this.isLoading = isLoading; + return this; + } + + /** See {@link ReceiverAppStateUpdate#playbackParameters}. */ + public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { + this.playbackParameters = playbackParameters; + return this; + } + + /** See {@link ReceiverAppStateUpdate#trackSelectionParameters} */ + public Builder setTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { + this.trackSelectionParameters = trackSelectionParameters; + return this; + } + + /** See {@link ReceiverAppStateUpdate#errorMessage}. */ + public Builder setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } + + /** See {@link ReceiverAppStateUpdate#discontinuityReason}. */ + public Builder setDiscontinuityReason(Integer discontinuityReason) { + this.discontinuityReason = discontinuityReason; + return this; + } + + /** + * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link + * ReceiverAppStateUpdate#currentPlaybackPositionMs}. + */ + public Builder setPlaybackPosition( + UUID currentPlayingItemUuid, + String currentPlayingPeriodId, + Long currentPlaybackPositionMs) { + this.currentPlayingItemUuid = currentPlayingItemUuid; + this.currentPlayingPeriodId = currentPlayingPeriodId; + this.currentPlaybackPositionMs = currentPlaybackPositionMs; + return this; + } + + /** + * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link + * ReceiverAppStateUpdate#currentPlaybackPositionMs}. + */ + public Builder setMediaItemsInformation(Map mediaItemsInformation) { + this.mediaItemsInformation = Collections.unmodifiableMap(mediaItemsInformation); + return this; + } + + /** See {@link ReceiverAppStateUpdate#shuffleOrder}. */ + public Builder setShuffleOrder(List shuffleOrder) { + this.shuffleOrder = Collections.unmodifiableList(shuffleOrder); + return this; + } + + /** + * Returns a new {@link ReceiverAppStateUpdate} instance with the current values in this + * builder. + */ + public ReceiverAppStateUpdate build() { + return new ReceiverAppStateUpdate( + sequenceNumber, + playWhenReady, + playbackState, + items, + repeatMode, + shuffleModeEnabled, + isLoading, + playbackParameters, + trackSelectionParameters, + errorMessage, + discontinuityReason, + currentPlayingItemUuid, + currentPlayingPeriodId, + currentPlaybackPositionMs, + mediaItemsInformation, + shuffleOrder); + } + } + + /** Returns a {@link ReceiverAppStateUpdate} builder. */ + public static Builder builder(long sequenceNumber) { + return new Builder(sequenceNumber); + } + + /** + * Creates an instance from parsing a state update received from the Receiver App. + * + * @param jsonMessage The state update encoded as a JSON string. + * @return The parsed state update. + * @throws JSONException If an error is encountered when parsing the {@code jsonMessage}. + */ + public static ReceiverAppStateUpdate fromJsonMessage(String jsonMessage) throws JSONException { + JSONObject stateAsJson = new JSONObject(jsonMessage); + Builder builder = builder(stateAsJson.getLong(KEY_SEQUENCE_NUMBER)); + + if (stateAsJson.has(KEY_PLAY_WHEN_READY)) { + builder.setPlayWhenReady(stateAsJson.getBoolean(KEY_PLAY_WHEN_READY)); + } + + if (stateAsJson.has(KEY_PLAYBACK_STATE)) { + builder.setPlaybackState( + playbackStateStringToConstant(stateAsJson.getString(KEY_PLAYBACK_STATE))); + } + + if (stateAsJson.has(KEY_MEDIA_QUEUE)) { + builder.setItems( + toMediaItemArrayList(Assertions.checkNotNull(stateAsJson.optJSONArray(KEY_MEDIA_QUEUE)))); + } + + if (stateAsJson.has(KEY_REPEAT_MODE)) { + builder.setRepeatMode(stringToRepeatMode(stateAsJson.getString(KEY_REPEAT_MODE))); + } + + if (stateAsJson.has(KEY_SHUFFLE_MODE_ENABLED)) { + builder.setShuffleModeEnabled(stateAsJson.getBoolean(KEY_SHUFFLE_MODE_ENABLED)); + } + + if (stateAsJson.has(KEY_IS_LOADING)) { + builder.setIsLoading(stateAsJson.getBoolean(KEY_IS_LOADING)); + } + + if (stateAsJson.has(KEY_PLAYBACK_PARAMETERS)) { + builder.setPlaybackParameters( + toPlaybackParameters( + Assertions.checkNotNull(stateAsJson.optJSONObject(KEY_PLAYBACK_PARAMETERS)))); + } + + if (stateAsJson.has(KEY_TRACK_SELECTION_PARAMETERS)) { + JSONObject trackSelectionParametersJson = + stateAsJson.getJSONObject(KEY_TRACK_SELECTION_PARAMETERS); + TrackSelectionParameters parameters = + TrackSelectionParameters.DEFAULT + .buildUpon() + .setPreferredTextLanguage( + trackSelectionParametersJson.getString(KEY_PREFERRED_TEXT_LANGUAGE)) + .setPreferredAudioLanguage( + trackSelectionParametersJson.getString(KEY_PREFERRED_AUDIO_LANGUAGE)) + .setSelectUndeterminedTextLanguage( + trackSelectionParametersJson.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)) + .setDisabledTextTrackSelectionFlags( + jsonArrayToSelectionFlags( + trackSelectionParametersJson.getJSONArray( + KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS))) + .build(); + builder.setTrackSelectionParameters(parameters); + } + + if (stateAsJson.has(KEY_ERROR_MESSAGE)) { + builder.setErrorMessage(stateAsJson.getString(KEY_ERROR_MESSAGE)); + } + + if (stateAsJson.has(KEY_PLAYBACK_POSITION)) { + JSONObject playbackPosition = stateAsJson.getJSONObject(KEY_PLAYBACK_POSITION); + String discontinuityReason = playbackPosition.optString(KEY_DISCONTINUITY_REASON); + if (!discontinuityReason.isEmpty()) { + builder.setDiscontinuityReason(stringToDiscontinuityReason(discontinuityReason)); + } + UUID currentPlayingItemUuid = UUID.fromString(playbackPosition.getString(KEY_UUID)); + String currentPlayingPeriodId = playbackPosition.getString(KEY_PERIOD_ID); + Long currentPlaybackPositionMs = playbackPosition.getLong(KEY_POSITION_MS); + builder.setPlaybackPosition( + currentPlayingItemUuid, currentPlayingPeriodId, currentPlaybackPositionMs); + } + + if (stateAsJson.has(KEY_MEDIA_ITEMS_INFO)) { + HashMap mediaItemInformation = new HashMap<>(); + JSONObject mediaItemsInfo = stateAsJson.getJSONObject(KEY_MEDIA_ITEMS_INFO); + for (Iterator i = mediaItemsInfo.keys(); i.hasNext(); ) { + String key = i.next(); + mediaItemInformation.put( + UUID.fromString(key), jsonToMediaitemInfo(mediaItemsInfo.getJSONObject(key))); + } + builder.setMediaItemsInformation(mediaItemInformation); + } + + if (stateAsJson.has(KEY_SHUFFLE_ORDER)) { + ArrayList shuffleOrder = new ArrayList<>(); + JSONArray shuffleOrderJson = stateAsJson.getJSONArray(KEY_SHUFFLE_ORDER); + for (int i = 0; i < shuffleOrderJson.length(); i++) { + shuffleOrder.add(shuffleOrderJson.getInt(i)); + } + builder.setShuffleOrder(shuffleOrder); + } + + return builder.build(); + } + + /** The sequence number of the status update. */ + public final long sequenceNumber; + /** Optional {@link Player#getPlayWhenReady playWhenReady} value. */ + @Nullable public final Boolean playWhenReady; + /** Optional {@link Player#getPlaybackState() playbackState}. */ + @Nullable public final Integer playbackState; + /** Optional list of media items. */ + @Nullable public final List items; + /** Optional {@link Player#getRepeatMode() repeatMode}. */ + @Nullable public final Integer repeatMode; + /** Optional {@link Player#getShuffleModeEnabled() shuffleMode}. */ + @Nullable public final Boolean shuffleModeEnabled; + /** Optional {@link Player#isLoading() isLoading} value. */ + @Nullable public final Boolean isLoading; + /** Optional {@link Player#getPlaybackParameters() playbackParameters}. */ + @Nullable public final PlaybackParameters playbackParameters; + /** Optional {@link TrackSelectionParameters}. */ + @Nullable public final TrackSelectionParameters trackSelectionParameters; + /** Optional error message string. */ + @Nullable public final String errorMessage; + /** + * Optional reason for a {@link Player.EventListener#onPositionDiscontinuity(int) discontinuity } + * in the playback position. + */ + @Nullable public final Integer discontinuityReason; + /** Optional {@link UUID} of the {@link Player#getCurrentWindowIndex() currently played item}. */ + @Nullable public final UUID currentPlayingItemUuid; + /** Optional id of the current {@link Player#getCurrentPeriodIndex() period being played}. */ + @Nullable public final String currentPlayingPeriodId; + /** Optional {@link Player#getCurrentPosition() playbackPosition} in milliseconds. */ + @Nullable public final Long currentPlaybackPositionMs; + /** Holds information about the {@link MediaItem media items} in the media queue. */ + public final Map mediaItemsInformation; + /** Holds the indices of the media queue items in shuffle order. */ + @Nullable public final List shuffleOrder; + + /** Creates an instance with the given values. */ + private ReceiverAppStateUpdate( + long sequenceNumber, + @Nullable Boolean playWhenReady, + @Nullable Integer playbackState, + @Nullable List items, + @Nullable Integer repeatMode, + @Nullable Boolean shuffleModeEnabled, + @Nullable Boolean isLoading, + @Nullable PlaybackParameters playbackParameters, + @Nullable TrackSelectionParameters trackSelectionParameters, + @Nullable String errorMessage, + @Nullable Integer discontinuityReason, + @Nullable UUID currentPlayingItemUuid, + @Nullable String currentPlayingPeriodId, + @Nullable Long currentPlaybackPositionMs, + Map mediaItemsInformation, + @Nullable List shuffleOrder) { + this.sequenceNumber = sequenceNumber; + this.playWhenReady = playWhenReady; + this.playbackState = playbackState; + this.items = items; + this.repeatMode = repeatMode; + this.shuffleModeEnabled = shuffleModeEnabled; + this.isLoading = isLoading; + this.playbackParameters = playbackParameters; + this.trackSelectionParameters = trackSelectionParameters; + this.errorMessage = errorMessage; + this.discontinuityReason = discontinuityReason; + this.currentPlayingItemUuid = currentPlayingItemUuid; + this.currentPlayingPeriodId = currentPlayingPeriodId; + this.currentPlaybackPositionMs = currentPlaybackPositionMs; + this.mediaItemsInformation = mediaItemsInformation; + this.shuffleOrder = shuffleOrder; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ReceiverAppStateUpdate that = (ReceiverAppStateUpdate) other; + + return sequenceNumber == that.sequenceNumber + && Util.areEqual(playWhenReady, that.playWhenReady) + && Util.areEqual(playbackState, that.playbackState) + && Util.areEqual(items, that.items) + && Util.areEqual(repeatMode, that.repeatMode) + && Util.areEqual(shuffleModeEnabled, that.shuffleModeEnabled) + && Util.areEqual(isLoading, that.isLoading) + && Util.areEqual(playbackParameters, that.playbackParameters) + && Util.areEqual(trackSelectionParameters, that.trackSelectionParameters) + && Util.areEqual(errorMessage, that.errorMessage) + && Util.areEqual(discontinuityReason, that.discontinuityReason) + && Util.areEqual(currentPlayingItemUuid, that.currentPlayingItemUuid) + && Util.areEqual(currentPlayingPeriodId, that.currentPlayingPeriodId) + && Util.areEqual(currentPlaybackPositionMs, that.currentPlaybackPositionMs) + && Util.areEqual(mediaItemsInformation, that.mediaItemsInformation) + && Util.areEqual(shuffleOrder, that.shuffleOrder); + } + + @Override + public int hashCode() { + int result = (int) (sequenceNumber ^ (sequenceNumber >>> 32)); + result = 31 * result + (playWhenReady != null ? playWhenReady.hashCode() : 0); + result = 31 * result + (playbackState != null ? playbackState.hashCode() : 0); + result = 31 * result + (items != null ? items.hashCode() : 0); + result = 31 * result + (repeatMode != null ? repeatMode.hashCode() : 0); + result = 31 * result + (shuffleModeEnabled != null ? shuffleModeEnabled.hashCode() : 0); + result = 31 * result + (isLoading != null ? isLoading.hashCode() : 0); + result = 31 * result + (playbackParameters != null ? playbackParameters.hashCode() : 0); + result = + 31 * result + (trackSelectionParameters != null ? trackSelectionParameters.hashCode() : 0); + result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0); + result = 31 * result + (discontinuityReason != null ? discontinuityReason.hashCode() : 0); + result = 31 * result + (currentPlayingItemUuid != null ? currentPlayingItemUuid.hashCode() : 0); + result = 31 * result + (currentPlayingPeriodId != null ? currentPlayingPeriodId.hashCode() : 0); + result = + 31 * result + + (currentPlaybackPositionMs != null ? currentPlaybackPositionMs.hashCode() : 0); + result = 31 * result + mediaItemsInformation.hashCode(); + result = 31 * result + (shuffleOrder != null ? shuffleOrder.hashCode() : 0); + return result; + } + + // Internal methods. + + @VisibleForTesting + /* package */ static List toMediaItemArrayList(JSONArray mediaItemsAsJson) + throws JSONException { + ArrayList mediaItems = new ArrayList<>(); + for (int i = 0; i < mediaItemsAsJson.length(); i++) { + mediaItems.add(toMediaItem(mediaItemsAsJson.getJSONObject(i))); + } + return mediaItems; + } + + private static MediaItem toMediaItem(JSONObject mediaItemAsJson) throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + builder.setUuid(UUID.fromString(mediaItemAsJson.getString(KEY_UUID))); + builder.setTitle(mediaItemAsJson.getString(KEY_TITLE)); + builder.setDescription(mediaItemAsJson.getString(KEY_DESCRIPTION)); + builder.setMedia(jsonToUriBundle(mediaItemAsJson.getJSONObject(KEY_MEDIA))); + // TODO(Internal b/118431961): Add attachment management. + + builder.setDrmSchemes(jsonArrayToDrmSchemes(mediaItemAsJson.getJSONArray(KEY_DRM_SCHEMES))); + if (mediaItemAsJson.has(KEY_START_POSITION_US)) { + builder.setStartPositionUs(mediaItemAsJson.getLong(KEY_START_POSITION_US)); + } + if (mediaItemAsJson.has(KEY_END_POSITION_US)) { + builder.setEndPositionUs(mediaItemAsJson.getLong(KEY_END_POSITION_US)); + } + builder.setMimeType(mediaItemAsJson.getString(KEY_MIME_TYPE)); + return builder.build(); + } + + private static PlaybackParameters toPlaybackParameters(JSONObject parameters) + throws JSONException { + float speed = (float) parameters.getDouble(KEY_SPEED); + float pitch = (float) parameters.getDouble(KEY_PITCH); + boolean skipSilence = parameters.getBoolean(KEY_SKIP_SILENCE); + return new PlaybackParameters(speed, pitch, skipSilence); + } + + private static int playbackStateStringToConstant(String string) { + switch (string) { + case STR_STATE_IDLE: + return STATE_IDLE; + case STR_STATE_BUFFERING: + return STATE_BUFFERING; + case STR_STATE_READY: + return STATE_READY; + case STR_STATE_ENDED: + return STATE_ENDED; + default: + throw new AssertionError("Unexpected state string: " + string); + } + } + + private static Integer stringToRepeatMode(String repeatModeStr) { + switch (repeatModeStr) { + case STR_REPEAT_MODE_OFF: + return REPEAT_MODE_OFF; + case STR_REPEAT_MODE_ONE: + return REPEAT_MODE_ONE; + case STR_REPEAT_MODE_ALL: + return REPEAT_MODE_ALL; + default: + throw new AssertionError("Illegal repeat mode: " + repeatModeStr); + } + } + + private static Integer stringToDiscontinuityReason(String discontinuityReasonStr) { + switch (discontinuityReasonStr) { + case STR_DISCONTINUITY_REASON_PERIOD_TRANSITION: + return DISCONTINUITY_REASON_PERIOD_TRANSITION; + case STR_DISCONTINUITY_REASON_SEEK: + return DISCONTINUITY_REASON_SEEK; + case STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return DISCONTINUITY_REASON_SEEK_ADJUSTMENT; + case STR_DISCONTINUITY_REASON_AD_INSERTION: + return DISCONTINUITY_REASON_AD_INSERTION; + case STR_DISCONTINUITY_REASON_INTERNAL: + return DISCONTINUITY_REASON_INTERNAL; + default: + throw new AssertionError("Illegal discontinuity reason: " + discontinuityReasonStr); + } + } + + @C.SelectionFlags + private static int jsonArrayToSelectionFlags(JSONArray array) throws JSONException { + int result = 0; + for (int i = 0; i < array.length(); i++) { + switch (array.getString(i)) { + case ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT: + result |= C.SELECTION_FLAG_AUTOSELECT; + break; + case ExoCastConstants.STR_SELECTION_FLAG_FORCED: + result |= C.SELECTION_FLAG_FORCED; + break; + case ExoCastConstants.STR_SELECTION_FLAG_DEFAULT: + result |= C.SELECTION_FLAG_DEFAULT; + break; + default: + // Do nothing. + break; + } + } + return result; + } + + private static List jsonArrayToDrmSchemes(JSONArray drmSchemesAsJson) + throws JSONException { + ArrayList drmSchemes = new ArrayList<>(); + for (int i = 0; i < drmSchemesAsJson.length(); i++) { + JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); + MediaItem.UriBundle uriBundle = + drmSchemeAsJson.has(KEY_LICENSE_SERVER) + ? jsonToUriBundle(drmSchemeAsJson.getJSONObject(KEY_LICENSE_SERVER)) + : null; + drmSchemes.add( + new MediaItem.DrmScheme(UUID.fromString(drmSchemeAsJson.getString(KEY_UUID)), uriBundle)); + } + return Collections.unmodifiableList(drmSchemes); + } + + private static MediaItem.UriBundle jsonToUriBundle(JSONObject json) throws JSONException { + Uri uri = Uri.parse(json.getString(KEY_URI)); + JSONObject requestHeadersAsJson = json.getJSONObject(KEY_REQUEST_HEADERS); + HashMap requestHeaders = new HashMap<>(); + for (Iterator i = requestHeadersAsJson.keys(); i.hasNext(); ) { + String key = i.next(); + requestHeaders.put(key, requestHeadersAsJson.getString(key)); + } + return new MediaItem.UriBundle(uri, requestHeaders); + } + + private static MediaItemInfo jsonToMediaitemInfo(JSONObject json) throws JSONException { + long durationUs = json.getLong(KEY_WINDOW_DURATION_US); + long defaultPositionUs = json.optLong(KEY_DEFAULT_START_POSITION_US, /* fallback= */ 0L); + JSONArray periodsJson = json.getJSONArray(KEY_PERIODS); + ArrayList periods = new ArrayList<>(); + long positionInFirstPeriodUs = json.getLong(KEY_POSITION_IN_FIRST_PERIOD_US); + + long windowPositionUs = -positionInFirstPeriodUs; + for (int i = 0; i < periodsJson.length(); i++) { + JSONObject periodJson = periodsJson.getJSONObject(i); + long periodDurationUs = periodJson.optLong(KEY_DURATION_US, C.TIME_UNSET); + periods.add( + new MediaItemInfo.Period( + periodJson.getString(KEY_ID), periodDurationUs, windowPositionUs)); + windowPositionUs += periodDurationUs; + } + boolean isDynamic = json.getBoolean(KEY_IS_DYNAMIC); + boolean isSeekable = json.getBoolean(KEY_IS_SEEKABLE); + return new MediaItemInfo( + durationUs, defaultPositionUs, periods, positionInFirstPeriodUs, isSeekable, isDynamic); + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java new file mode 100644 index 0000000000..b900a78937 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java @@ -0,0 +1,436 @@ +/* + * 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; + +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmScheme; +import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.MimeTypes; +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 java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link ExoCastMessage}. */ +@RunWith(AndroidJUnit4.class) +public class ExoCastMessageTest { + + @Test + public void addItems_withUnsetIndex_doesNotAddIndexToJson() throws JSONException { + MediaItem sampleItem = new MediaItem.Builder().build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Collections.singletonList(sampleItem), + new ShuffleOrder.UnshuffledShuffleOrder(1)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); + assertThat(arguments.has(KEY_INDEX)).isFalse(); + assertThat(items.length()).isEqualTo(1); + } + + @Test + public void addItems_withMultipleItems_producesExpectedJsonList() throws JSONException { + MediaItem sampleItem1 = new MediaItem.Builder().build(); + MediaItem sampleItem2 = new MediaItem.Builder().build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + 1, Arrays.asList(sampleItem2, sampleItem1), new ShuffleOrder.UnshuffledShuffleOrder(2)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); + assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(1); + assertThat(items.length()).isEqualTo(2); + } + + @Test + public void addItems_withoutItemOptionalFields_doesNotAddFieldsToJson() throws JSONException { + MediaItem itemWithoutOptionalFields = + new MediaItem.Builder() + .setTitle("title") + .setMimeType(MimeTypes.AUDIO_MP4) + .setDescription("desc") + .setDrmSchemes(Collections.singletonList(new DrmScheme(C.WIDEVINE_UUID, null))) + .setMedia("www.google.com") + .build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Collections.singletonList(itemWithoutOptionalFields), + new ShuffleOrder.UnshuffledShuffleOrder(1)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithoutOptionalFields); + } + + @Test + public void addItems_withAllItemFields_addsFieldsToJson() throws JSONException { + HashMap headersMedia = new HashMap<>(); + headersMedia.put("header1", "value1"); + headersMedia.put("header2", "value2"); + UriBundle media = new UriBundle(Uri.parse("www.google.com"), headersMedia); + + HashMap headersWidevine = new HashMap<>(); + headersWidevine.put("widevine", "value"); + UriBundle widevingUriBundle = new UriBundle(Uri.parse("www.widevine.com"), headersWidevine); + + HashMap headersPlayready = new HashMap<>(); + headersPlayready.put("playready", "value"); + UriBundle playreadyUriBundle = new UriBundle(Uri.parse("www.playready.com"), headersPlayready); + + DrmScheme[] drmSchemes = + new DrmScheme[] { + new DrmScheme(C.WIDEVINE_UUID, widevingUriBundle), + new DrmScheme(C.PLAYREADY_UUID, playreadyUriBundle) + }; + MediaItem itemWithAllFields = + new MediaItem.Builder() + .setTitle("title") + .setMimeType(MimeTypes.VIDEO_MP4) + .setDescription("desc") + .setStartPositionUs(3) + .setEndPositionUs(10) + .setDrmSchemes(Arrays.asList(drmSchemes)) + .setMedia(media) + .build(); + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Collections.singletonList(itemWithAllFields), + new ShuffleOrder.UnshuffledShuffleOrder(1)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + JSONArray items = arguments.getJSONArray(KEY_ITEMS); + + assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithAllFields); + } + + @Test + public void addItems_withShuffleOrder_producesExpectedJson() throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem sampleItem1 = builder.build(); + MediaItem sampleItem2 = builder.build(); + MediaItem sampleItem3 = builder.build(); + MediaItem sampleItem4 = builder.build(); + + ExoCastMessage message = + new ExoCastMessage.AddItems( + C.INDEX_UNSET, + Arrays.asList(sampleItem1, sampleItem2, sampleItem3, sampleItem4), + new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); + JSONObject arguments = + new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)).getJSONObject(KEY_ARGS); + JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); + assertThat(shuffledIndices.getInt(0)).isEqualTo(2); + assertThat(shuffledIndices.getInt(1)).isEqualTo(1); + assertThat(shuffledIndices.getInt(2)).isEqualTo(3); + assertThat(shuffledIndices.getInt(3)).isEqualTo(0); + } + + @Test + public void moveItem_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.MoveItem( + new UUID(0, 1), + /* index= */ 3, + new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_MOVE_ITEM); + assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); + assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(3); + JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); + assertThat(shuffledIndices.getInt(0)).isEqualTo(2); + assertThat(shuffledIndices.getInt(1)).isEqualTo(1); + assertThat(shuffledIndices.getInt(2)).isEqualTo(3); + assertThat(shuffledIndices.getInt(3)).isEqualTo(0); + } + + @Test + public void removeItems_withSingleItem_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.RemoveItems(Collections.singletonList(new UUID(0, 1))); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); + assertThat(uuids.length()).isEqualTo(1); + assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); + } + + @Test + public void removeItems_withMultipleItems_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.RemoveItems( + Arrays.asList(new UUID(0, 1), new UUID(0, 2), new UUID(0, 3))); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); + assertThat(uuids.length()).isEqualTo(3); + assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); + assertThat(uuids.getString(1)).isEqualTo(new UUID(0, 2).toString()); + assertThat(uuids.getString(2)).isEqualTo(new UUID(0, 3).toString()); + } + + @Test + public void setPlayWhenReady_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetPlayWhenReady(true); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAY_WHEN_READY); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_PLAY_WHEN_READY)).isTrue(); + } + + @Test + public void setRepeatMode_withRepeatModeOff_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_OFF); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) + .isEqualTo(STR_REPEAT_MODE_OFF); + } + + @Test + public void setRepeatMode_withRepeatModeOne_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ONE); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) + .isEqualTo(STR_REPEAT_MODE_ONE); + } + + @Test + public void setRepeatMode_withRepeatModeAll_producesExpectedJson() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ALL); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) + .isEqualTo(STR_REPEAT_MODE_ALL); + } + + @Test + public void setShuffleModeEnabled_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SetShuffleModeEnabled(/* shuffleModeEnabled= */ false); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_SHUFFLE_MODE_ENABLED); + assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_SHUFFLE_MODE_ENABLED)) + .isFalse(); + } + + @Test + public void seekTo_withPositionInItem_addsPositionField() throws JSONException { + ExoCastMessage message = new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ 10); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); + assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); + assertThat(arguments.getLong(KEY_POSITION_MS)).isEqualTo(10); + } + + @Test + public void seekTo_withUnsetPosition_doesNotAddPositionField() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ C.TIME_UNSET); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); + assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); + assertThat(arguments.has(KEY_POSITION_MS)).isFalse(); + } + + @Test + public void setPlaybackParameters_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SetPlaybackParameters( + new PlaybackParameters(/* speed= */ 0.5f, /* pitch= */ 2, /* skipSilence= */ false)); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAYBACK_PARAMETERS); + assertThat(arguments.getDouble(KEY_SPEED)).isEqualTo(0.5); + assertThat(arguments.getDouble(KEY_PITCH)).isEqualTo(2.0); + assertThat(arguments.getBoolean(KEY_SKIP_SILENCE)).isFalse(); + } + + @Test + public void setSelectionParameters_producesExpectedJson() throws JSONException { + ExoCastMessage message = + new ExoCastMessage.SetTrackSelectionParameters( + TrackSelectionParameters.DEFAULT + .buildUpon() + .setDisabledTextTrackSelectionFlags( + C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT) + .setSelectUndeterminedTextLanguage(true) + .setPreferredAudioLanguage("esp") + .setPreferredTextLanguage("deu") + .build()); + JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); + JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); + + assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); + assertThat(messageAsJson.getString(KEY_METHOD)) + .isEqualTo(METHOD_SET_TRACK_SELECTION_PARAMETERS); + assertThat(arguments.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)).isTrue(); + assertThat(arguments.getString(KEY_PREFERRED_AUDIO_LANGUAGE)).isEqualTo("esp"); + assertThat(arguments.getString(KEY_PREFERRED_TEXT_LANGUAGE)).isEqualTo("deu"); + ArrayList selectionFlagStrings = new ArrayList<>(); + JSONArray selectionFlagsJson = arguments.getJSONArray(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS); + for (int i = 0; i < selectionFlagsJson.length(); i++) { + selectionFlagStrings.add(selectionFlagsJson.getString(i)); + } + assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT); + assertThat(selectionFlagStrings).doesNotContain(ExoCastConstants.STR_SELECTION_FLAG_FORCED); + assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT); + } + + private static void assertJsonEqualsMediaItem(JSONObject itemAsJson, MediaItem mediaItem) + throws JSONException { + assertThat(itemAsJson.getString(KEY_UUID)).isEqualTo(mediaItem.uuid.toString()); + assertThat(itemAsJson.getString(KEY_TITLE)).isEqualTo(mediaItem.title); + assertThat(itemAsJson.getString(KEY_MIME_TYPE)).isEqualTo(mediaItem.mimeType); + assertThat(itemAsJson.getString(KEY_DESCRIPTION)).isEqualTo(mediaItem.description); + assertJsonMatchesTimestamp(itemAsJson, KEY_START_POSITION_US, mediaItem.startPositionUs); + assertJsonMatchesTimestamp(itemAsJson, KEY_END_POSITION_US, mediaItem.endPositionUs); + assertJsonMatchesUriBundle(itemAsJson, KEY_MEDIA, mediaItem.media); + + List drmSchemes = mediaItem.drmSchemes; + int drmSchemesLength = drmSchemes.size(); + JSONArray drmSchemesAsJson = itemAsJson.getJSONArray(KEY_DRM_SCHEMES); + + assertThat(drmSchemesAsJson.length()).isEqualTo(drmSchemesLength); + for (int i = 0; i < drmSchemesLength; i++) { + DrmScheme drmScheme = drmSchemes.get(i); + JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); + + assertThat(drmSchemeAsJson.getString(KEY_UUID)).isEqualTo(drmScheme.uuid.toString()); + assertJsonMatchesUriBundle(drmSchemeAsJson, KEY_LICENSE_SERVER, drmScheme.licenseServer); + } + } + + private static void assertJsonMatchesUriBundle( + JSONObject jsonObject, String key, @Nullable UriBundle uriBundle) throws JSONException { + if (uriBundle == null) { + assertThat(jsonObject.has(key)).isFalse(); + return; + } + JSONObject uriBundleAsJson = jsonObject.getJSONObject(key); + assertThat(uriBundleAsJson.getString(KEY_URI)).isEqualTo(uriBundle.uri.toString()); + Map requestHeaders = uriBundle.requestHeaders; + JSONObject requestHeadersAsJson = uriBundleAsJson.getJSONObject(KEY_REQUEST_HEADERS); + + assertThat(requestHeadersAsJson.length()).isEqualTo(requestHeaders.size()); + for (String headerKey : requestHeaders.keySet()) { + assertThat(requestHeadersAsJson.getString(headerKey)) + .isEqualTo(requestHeaders.get(headerKey)); + } + } + + private static void assertJsonMatchesTimestamp(JSONObject object, String key, long timestamp) + throws JSONException { + if (timestamp == C.TIME_UNSET) { + assertThat(object.has(key)).isFalse(); + } else { + assertThat(object.getLong(key)).isEqualTo(timestamp); + } + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java new file mode 100644 index 0000000000..58f78b090a --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java @@ -0,0 +1,1018 @@ +/* + * 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; + +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +/** Unit test for {@link ExoCastPlayer}. */ +@RunWith(AndroidJUnit4.class) +public class ExoCastPlayerTest { + + private static final long MOCK_SEQUENCE_NUMBER = 1; + private ExoCastPlayer player; + private MediaItem.Builder itemBuilder; + private CastSessionManager.StateListener receiverAppStateListener; + private FakeClock clock; + @Mock private CastSessionManager sessionManager; + @Mock private SessionAvailabilityListener sessionAvailabilityListener; + @Mock private Player.EventListener playerEventListener; + + @Before + public void setUp() { + initMocks(this); + clock = new FakeClock(/* initialTimeMs= */ 0); + player = + new ExoCastPlayer( + listener -> { + receiverAppStateListener = listener; + return sessionManager; + }, + clock); + player.addListener(playerEventListener); + itemBuilder = new MediaItem.Builder(); + } + + @Test + public void exoCastPlayer_startsAndStopsSessionManager() { + // The session manager should have been started when setting up, with the creation of + // ExoCastPlayer. + verify(sessionManager).start(); + verifyNoMoreInteractions(sessionManager); + player.release(); + verify(sessionManager).stopTrackingSession(); + verifyNoMoreInteractions(sessionManager); + } + + @Test + public void exoCastPlayer_propagatesSessionStatus() { + player.setSessionAvailabilityListener(sessionAvailabilityListener); + verify(sessionAvailabilityListener, never()).onCastSessionAvailable(); + receiverAppStateListener.onCastSessionAvailable(); + verify(sessionAvailabilityListener).onCastSessionAvailable(); + verifyNoMoreInteractions(sessionAvailabilityListener); + receiverAppStateListener.onCastSessionUnavailable(); + verify(sessionAvailabilityListener).onCastSessionUnavailable(); + verifyNoMoreInteractions(sessionAvailabilityListener); + } + + @Test + public void addItemsToQueue_producesExpectedMessages() throws JSONException { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); + + player.addItemsToQueue(item1, item2); + assertMediaItemQueue(item1, item2); + + player.addItemsToQueue(1, item3, item4); + assertMediaItemQueue(item1, item3, item4, item2); + + player.addItemsToQueue(item5); + assertMediaItemQueue(item1, item3, item4, item2, item5); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(3)).send(messageCaptor.capture()); + assertMessageAddsItems( + /* message= */ messageCaptor.getAllValues().get(0), + /* index= */ C.INDEX_UNSET, + Arrays.asList(item1, item2)); + assertMessageAddsItems( + /* message= */ messageCaptor.getAllValues().get(1), + /* index= */ 1, + Arrays.asList(item3, item4)); + assertMessageAddsItems( + /* message= */ messageCaptor.getAllValues().get(2), + /* index= */ C.INDEX_UNSET, + Collections.singletonList(item5)); + } + + @Test + public void addItemsToQueue_masksRemoteUpdates() { + player.prepare(); + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + + player.addItemsToQueue(item1, item2); + assertMediaItemQueue(item1, item2); + + // Should be ignored due to a lower sequence number. + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setItems(Arrays.asList(item3, item4)) + .build()); + + // Should override the current state. + assertMediaItemQueue(item1, item2); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setItems(Arrays.asList(item3, item4)) + .build()); + + assertMediaItemQueue(item3, item4); + } + + @Test + public void addItemsToQueue_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(2); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + player.addItemsToQueue(/* optionalIndex= */ 0, itemBuilder.build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(3); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); + + player.addItemsToQueue(itemBuilder.build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(3); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); + } + + @Test + public void addItemsToQueue_doesNotAddDuplicateUuids() { + player.prepare(); + player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); + assertThat(player.getQueueSize()).isEqualTo(1); + player.addItemsToQueue( + itemBuilder.setUuid(toUuid(1)).build(), itemBuilder.setUuid(toUuid(2)).build()); + assertThat(player.getQueueSize()).isEqualTo(2); + try { + player.addItemsToQueue( + itemBuilder.setUuid(toUuid(3)).build(), itemBuilder.setUuid(toUuid(3)).build()); + fail(); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void moveItemInQueue_behavesAsExpected() throws JSONException { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + player.addItemsToQueue(item1, item2, item3); + assertMediaItemQueue(item1, item2, item3); + player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); + assertMediaItemQueue(item2, item3, item1); + player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 1); + assertMediaItemQueue(item2, item3, item1); + player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); + assertMediaItemQueue(item3, item2, item1); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(4)).send(messageCaptor.capture()); + // First sent message is an "add" message. + assertMessageMovesItem( + /* message= */ messageCaptor.getAllValues().get(1), item1, /* index= */ 2); + assertMessageMovesItem( + /* message= */ messageCaptor.getAllValues().get(2), item3, /* index= */ 1); + assertMessageMovesItem( + /* message= */ messageCaptor.getAllValues().get(3), item3, /* index= */ 0); + } + + @Test + public void moveItemInQueue_moveBeforeToAfter_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 1); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + } + + @Test + public void moveItemInQueue_moveAfterToBefore_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + } + + @Test + public void moveItemInQueue_moveCurrent_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); + assertThat(player.getCurrentWindowIndex()).isEqualTo(2); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + } + + @Test + public void removeItemsFromQueue_masksMediaQueue() throws JSONException { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); + player.addItemsToQueue(item1, item2, item3, item4, item5); + assertMediaItemQueue(item1, item2, item3, item4, item5); + + player.removeItemFromQueue(2); + assertMediaItemQueue(item1, item2, item4, item5); + + player.removeRangeFromQueue(1, 3); + assertMediaItemQueue(item1, item5); + + player.clearQueue(); + assertMediaItemQueue(); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(4)).send(messageCaptor.capture()); + // First sent message is an "add" message. + assertMessageRemovesItems( + messageCaptor.getAllValues().get(1), Collections.singletonList(item3)); + assertMessageRemovesItems(messageCaptor.getAllValues().get(2), Arrays.asList(item2, item4)); + assertMessageRemovesItems(messageCaptor.getAllValues().get(3), Arrays.asList(item1, item5)); + } + + @Test + public void removeRangeFromQueue_beforeCurrentItem_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(2); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + } + + @Test + public void removeRangeFromQueue_currentItem_masksWindowIndexAsExpected() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + } + + @Test + public void removeRangeFromQueue_currentItemWhichIsLast_transitionsToEnded() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.removeRangeFromQueue(/* indexFrom= */ 1, /* indexExclusiveTo= */ 3); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void clearQueue_resetsPlaybackPosition() { + player.prepare(); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); + + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + player.clearQueue(); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void prepare_emptyQueue_transitionsToEnded() { + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + verify(playerEventListener).onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_ENDED); + } + + @Test + public void prepare_withQueue_transitionsToBuffering() { + player.addItemsToQueue(itemBuilder.build()); + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_BUFFERING); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); + } + + @Test + public void stop_withoutReset_leavesCurrentTimeline() { + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); + verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + player.stop(/* reset= */ false); + verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); + // Update for prepare. + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); + + // Update for stop. + verifyNoMoreInteractions(playerEventListener); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); + } + + @Test + public void stop_withReset_clearsQueue() { + player.prepare(); + player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); + verify(playerEventListener) + .onTimelineChanged( + any(Timeline.class), isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); + player.stop(/* reset= */ true); + verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); + + // Update for add. + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); + + // Update for stop. + verify(playerEventListener) + .onTimelineChanged( + argumentCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_RESET)); + assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(0); + + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + } + + @Test + public void getCurrentTimeline_masksRemoteUpdates() { + player.prepare(); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + player.addItemsToQueue(item1, item2); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + messageCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + Timeline reportedTimeline = messageCaptor.getValue(); + assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); + assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + } + + @Test + public void getCurrentTimeline_exposesReceiverState() { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setItems(Arrays.asList(item1, item2)) + .setShuffleOrder(Arrays.asList(1, 0)) + .build()); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); + verify(playerEventListener) + .onTimelineChanged( + messageCaptor.capture(), + /* manifest= */ isNull(), + eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + Timeline reportedTimeline = messageCaptor.getValue(); + assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); + assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) + .isEqualTo(C.TIME_UNSET); + } + + @Test + public void timelineUpdateFromReceiver_matchesLocalState_doesNotCallEventLsitener() { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + + MediaItemInfo.Period period1 = + new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period3 = + new MediaItemInfo.Period( + "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); + HashMap mediaItemInfoMap1 = new HashMap<>(); + mediaItemInfoMap1.put( + toUuid(1), + new MediaItemInfo( + /* windowDurationUs= */ 3000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false)); + mediaItemInfoMap1.put( + toUuid(3), + new MediaItemInfo( + /* windowDurationUs= */ 2000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(1) + .setPlaybackState(Player.STATE_BUFFERING) + .setItems(Arrays.asList(item1, item2, item3, item4)) + .setShuffleOrder(Arrays.asList(1, 0, 2, 3)) + .setMediaItemsInformation(mediaItemInfoMap1) + .build()); + verify(playerEventListener) + .onTimelineChanged( + any(), /* manifest= */ isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); + verify(playerEventListener) + .onPlayerStateChanged( + /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); + + HashMap mediaItemInfoMap2 = new HashMap<>(mediaItemInfoMap1); + mediaItemInfoMap2.put( + toUuid(5), + new MediaItemInfo( + /* windowDurationUs= */ 5, + /* defaultStartPositionUs= */ 0, + /* periods= */ Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(1).setMediaItemsInformation(mediaItemInfoMap2).build()); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void getPeriodIndex_producesExpectedOutput() { + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); + MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); + + MediaItemInfo.Period period1 = + new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period3 = + new MediaItemInfo.Period( + "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); + HashMap mediaItemInfoMap = new HashMap<>(); + mediaItemInfoMap.put( + toUuid(1), + new MediaItemInfo( + /* windowDurationUs= */ 3000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false)); + mediaItemInfoMap.put( + toUuid(3), + new MediaItemInfo( + /* windowDurationUs= */ 2000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1L) + .setPlaybackState(Player.STATE_BUFFERING) + .setItems(Arrays.asList(item1, item2, item3, item4)) + .setShuffleOrder(Arrays.asList(1, 0, 3, 2)) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition( + /* currentPlayingItemUuid= */ item3.uuid, + /* currentPlayingPeriodId= */ "id2", + /* currentPlaybackPositionMs= */ 500L) + .build()); + + assertThat(player.getCurrentPeriodIndex()).isEqualTo(5); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0L); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1500L); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + } + + @Test + public void exoCastPlayer_propagatesPlayerStateFromReceiver() { + ReceiverAppStateUpdate.Builder builder = + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1); + + // The first idle state update should be discarded, since it matches the current state. + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_IDLE).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_BUFFERING).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_READY).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + builder.setPlaybackState(Player.STATE_ENDED).build()); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Integer.class); + verify(playerEventListener, times(3)) + .onPlayerStateChanged(/* playWhenReady= */ eq(false), messageCaptor.capture()); + List states = messageCaptor.getAllValues(); + assertThat(states).hasSize(3); + assertThat(states) + .isEqualTo(Arrays.asList(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED)); + } + + @Test + public void setPlayWhenReady_changedLocally_notifiesListeners() { + player.setPlayWhenReady(false); + verify(playerEventListener, never()).onPlayerStateChanged(false, Player.STATE_IDLE); + player.setPlayWhenReady(true); + verify(playerEventListener).onPlayerStateChanged(true, Player.STATE_IDLE); + player.setPlayWhenReady(false); + verify(playerEventListener).onPlayerStateChanged(false, Player.STATE_IDLE); + } + + @Test + public void setPlayWhenReady_changedRemotely_notifiesListeners() { + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(false).build()); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void getPlayWhenReady_masksRemoteUpdates() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + player.setPlayWhenReady(true); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2).setPlayWhenReady(false).build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(true).build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(false).build()); + verify(playerEventListener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + } + + @Test + public void setRepeatMode_changedLocally_notifiesListeners() { + player.setRepeatMode(Player.REPEAT_MODE_OFF); + verifyNoMoreInteractions(playerEventListener); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void setRepeatMode_changedRemotely_notifiesListeners() { + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .build()); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + } + + @Test + public void getRepeatMode_masksRemoteUpdates() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .build()); + verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + } + + @Test + public void getPlaybackPosition_withStateChanges_producesExpectedOutput() { + UUID uuid = toUuid(1); + HashMap mediaItemInfoMap = new HashMap<>(); + + MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); + MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); + MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); + mediaItemInfoMap.put( + uuid, + new MediaItemInfo( + /* windowDurationUs= */ 1000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(1L); + player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) + .build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000L); + clock.advanceTime(/* timeDiffMs= */ 1L); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_READY) + .build()); + // Play when ready is still false, so position should not change. + assertThat(player.getCurrentPosition()).isEqualTo(1000L); + player.setPlayWhenReady(true); + clock.advanceTime(1); + assertThat(player.getCurrentPosition()).isEqualTo(1001L); + clock.advanceTime(1); + assertThat(player.getCurrentPosition()).isEqualTo(1002L); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1010L) + .build()); + clock.advanceTime(1); + assertThat(player.getCurrentPosition()).isEqualTo(1010L); + clock.advanceTime(1); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_READY) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1011L) + .build()); + clock.advanceTime(10); + assertThat(player.getCurrentPosition()).isEqualTo(1021L); + } + + @Test + public void getPlaybackPosition_withNonDefaultPlaybackSpeed_producesExpectedOutput() { + MediaItem item = itemBuilder.setUuid(toUuid(1)).build(); + MediaItemInfo info = + new MediaItemInfo( + /* windowDurationUs= */ 10000000, + /* defaultStartPositionUs= */ 3000000, + /* periods= */ Collections.singletonList( + new MediaItemInfo.Period( + /* id= */ "id", /* durationUs= */ 10000000, /* positionInWindowUs= */ 0)), + /* positionInFirstPeriodUs= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setMediaItemsInformation(Collections.singletonMap(toUuid(1), info)) + .setShuffleOrder(Collections.singletonList(0)) + .setItems(Collections.singletonList(item)) + .setPlaybackPosition( + toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 20L) + .setPlaybackState(Player.STATE_READY) + .setPlayWhenReady(true) + .build()); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(20); + clock.advanceTime(10); + assertThat(player.getCurrentPosition()).isEqualTo(30); + clock.advanceTime(10); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(1) + .setPlaybackPosition( + toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 40L) + .setPlaybackParameters(new PlaybackParameters(2)) + .build()); + clock.advanceTime(10); + assertThat(player.getCurrentPosition()).isEqualTo(60); + } + + @Test + public void positionChanges_notifiesDiscontinuities() { + UUID uuid = toUuid(1); + HashMap mediaItemInfoMap = new HashMap<>(); + + MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); + MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); + MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); + mediaItemInfoMap.put( + uuid, + new MediaItemInfo( + /* windowDurationUs= */ 1000L, + /* defaultStartPositionUs= */ 10, + /* periods= */ Arrays.asList(period1, period2, period3), + /* positionInFirstPeriodUs= */ 500, + /* isSeekable= */ true, + /* isDynamic= */ false)); + + player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) + .setPlaybackState(Player.STATE_BUFFERING) + .setMediaItemsInformation(mediaItemInfoMap) + .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) + .build()); + verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 999); + verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + } + + @Test + public void setShuffleModeEnabled_changedLocally_notifiesListeners() { + player.setShuffleModeEnabled(true); + verify(playerEventListener).onShuffleModeEnabledChanged(true); + player.setShuffleModeEnabled(true); + verifyNoMoreInteractions(playerEventListener); + } + + @Test + public void setShuffleModeEnabled_changedRemotely_notifiesListeners() { + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) + .setShuffleModeEnabled(true) + .build()); + verify(playerEventListener).onShuffleModeEnabledChanged(true); + assertThat(player.getShuffleModeEnabled()).isTrue(); + } + + @Test + public void getShuffleMode_masksRemoteUpdates() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + player.setShuffleModeEnabled(true); + assertThat(player.getShuffleModeEnabled()).isTrue(); + verify(playerEventListener).onShuffleModeEnabledChanged(true); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setShuffleModeEnabled(false) + .build()); + verifyNoMoreInteractions(playerEventListener); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setShuffleModeEnabled(false) + .build()); + verify(playerEventListener).onShuffleModeEnabledChanged(false); + assertThat(player.getShuffleModeEnabled()).isFalse(); + } + + @Test + public void seekTo_inIdle_doesNotChangePlaybackState() { + player.prepare(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + player.addItemsToQueue(itemBuilder.build(), itemBuilder.build()); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + player.stop(false); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + } + + @Test + public void seekTo_withTwoItems_producesExpectedMessage() { + player.prepare(); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + player.addItemsToQueue(item1, item2); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager, times(3)).send(messageCaptor.capture()); + // Messages should be prepare, add and seek. + ExoCastMessage.SeekTo seekToMessage = + (ExoCastMessage.SeekTo) messageCaptor.getAllValues().get(2); + assertThat(seekToMessage.positionMs).isEqualTo(1000); + assertThat(seekToMessage.uuid).isEqualTo(toUuid(2)); + } + + @Test + public void seekTo_masksRemoteUpdates() { + player.prepare(); + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); + MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); + player.addItemsToQueue(item1, item2); + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000L); + verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(playerEventListener) + .onPlayerStateChanged( + /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) + .setPlaybackPosition(toUuid(1), "id", 500L) + .build()); + assertThat(player.getCurrentWindowIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setPlaybackPosition(toUuid(1), "id", 500L) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) + .build()); + verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + assertThat(player.getCurrentWindowIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(500); + } + + @Test + public void setPlaybackParameters_producesExpectedMessage() { + PlaybackParameters playbackParameters = + new PlaybackParameters(/* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ true); + player.setPlaybackParameters(playbackParameters); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); + verify(sessionManager).send(messageCaptor.capture()); + ExoCastMessage.SetPlaybackParameters message = + (ExoCastMessage.SetPlaybackParameters) messageCaptor.getValue(); + assertThat(message.playbackParameters).isEqualTo(playbackParameters); + } + + @Test + public void getTrackSelectionParameters_doesNotOverrideUnexpectedFields() { + when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); + DefaultTrackSelector.Parameters parameters = + DefaultTrackSelector.Parameters.DEFAULT + .buildUpon() + .setPreferredAudioLanguage("spa") + .setMaxVideoSize(/* maxVideoWidth= */ 3, /* maxVideoHeight= */ 3) + .build(); + player.setTrackSelectionParameters(parameters); + TrackSelectionParameters returned = + TrackSelectionParameters.DEFAULT.buildUpon().setPreferredAudioLanguage("deu").build(); + receiverAppStateListener.onStateUpdateFromReceiverApp( + ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) + .setTrackSelectionParameters(returned) + .build()); + DefaultTrackSelector.Parameters result = + (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); + assertThat(result.preferredAudioLanguage).isEqualTo("deu"); + assertThat(result.maxVideoHeight).isEqualTo(3); + assertThat(result.maxVideoWidth).isEqualTo(3); + } + + @Test + public void testExoCast_getRendererType() { + assertThat(player.getRendererCount()).isEqualTo(4); + assertThat(player.getRendererType(/* index= */ 0)).isEqualTo(C.TRACK_TYPE_VIDEO); + assertThat(player.getRendererType(/* index= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO); + assertThat(player.getRendererType(/* index= */ 2)).isEqualTo(C.TRACK_TYPE_TEXT); + assertThat(player.getRendererType(/* index= */ 3)).isEqualTo(C.TRACK_TYPE_METADATA); + } + + private static UUID toUuid(long lowerBits) { + return new UUID(0, lowerBits); + } + + private void assertMediaItemQueue(MediaItem... mediaItemQueue) { + assertThat(player.getQueueSize()).isEqualTo(mediaItemQueue.length); + for (int i = 0; i < mediaItemQueue.length; i++) { + assertThat(player.getQueueItem(i).uuid).isEqualTo(mediaItemQueue[i].uuid); + } + } + + private static void assertMessageAddsItems( + ExoCastMessage message, int index, List mediaItems) throws JSONException { + assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_ADD_ITEMS); + JSONObject args = + new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); + if (index != C.INDEX_UNSET) { + assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); + } else { + assertThat(args.has(KEY_INDEX)).isFalse(); + } + JSONArray itemsAsJson = args.getJSONArray(KEY_ITEMS); + assertThat(ReceiverAppStateUpdate.toMediaItemArrayList(itemsAsJson)).isEqualTo(mediaItems); + } + + private static void assertMessageMovesItem(ExoCastMessage message, MediaItem item, int index) + throws JSONException { + assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_MOVE_ITEM); + JSONObject args = + new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); + assertThat(args.getString(KEY_UUID)).isEqualTo(item.uuid.toString()); + assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); + } + + private static void assertMessageRemovesItems(ExoCastMessage message, List items) + throws JSONException { + assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_REMOVE_ITEMS); + JSONObject args = + new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); + JSONArray uuidsAsJson = args.getJSONArray(KEY_UUIDS); + for (int i = 0; i < uuidsAsJson.length(); i++) { + assertThat(uuidsAsJson.getString(i)).isEqualTo(items.get(i).uuid.toString()); + } + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java new file mode 100644 index 0000000000..f6084339e4 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java @@ -0,0 +1,466 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ExoCastTimeline}. */ +@RunWith(AndroidJUnit4.class) +public class ExoCastTimelineTest { + + private MediaItem mediaItem1; + private MediaItem mediaItem2; + private MediaItem mediaItem3; + private MediaItem mediaItem4; + private MediaItem mediaItem5; + + @Before + public void setUp() { + MediaItem.Builder builder = new MediaItem.Builder(); + mediaItem1 = builder.setUuid(asUUID(1)).build(); + mediaItem2 = builder.setUuid(asUUID(2)).build(); + mediaItem3 = builder.setUuid(asUUID(3)).build(); + mediaItem4 = builder.setUuid(asUUID(4)).build(); + mediaItem5 = builder.setUuid(asUUID(5)).build(); + } + + @Test + public void getWindowCount_withNoItems_producesExpectedCount() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Collections.emptyList(), Collections.emptyMap(), new DefaultShuffleOrder(0)); + + assertThat(timeline.getWindowCount()).isEqualTo(0); + } + + @Test + public void getWindowCount_withFiveItems_producesExpectedCount() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + + assertThat(timeline.getWindowCount()).isEqualTo(5); + } + + @Test + public void getWindow_withNoMediaItemInfo_returnsEmptyWindow() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + Timeline.Window window = timeline.getWindow(2, new Timeline.Window(), /* setTag= */ true); + + assertThat(window.tag).isNull(); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.isSeekable).isFalse(); + assertThat(window.isDynamic).isTrue(); + assertThat(window.defaultPositionUs).isEqualTo(0L); + assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window.firstPeriodIndex).isEqualTo(2); + assertThat(window.lastPeriodIndex).isEqualTo(2); + assertThat(window.positionInFirstPeriodUs).isEqualTo(0L); + } + + @Test + public void getWindow_withMediaItemInfo_returnsPopulatedWindow() { + MediaItem populatedMediaItem = new MediaItem.Builder().setAttachment("attachment").build(); + HashMap mediaItemInfos = new HashMap<>(); + MediaItemInfo.Period period1 = + new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + mediaItemInfos.put( + populatedMediaItem.uuid, + new MediaItemInfo( + /* windowDurationUs= */ 4000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 500L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, populatedMediaItem), + mediaItemInfos, + new DefaultShuffleOrder(5)); + Timeline.Window window = timeline.getWindow(4, new Timeline.Window(), /* setTag= */ true); + + assertThat(window.tag).isSameInstanceAs(populatedMediaItem.attachment); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.isSeekable).isTrue(); + assertThat(window.isDynamic).isFalse(); + assertThat(window.defaultPositionUs).isEqualTo(20L); + assertThat(window.durationUs).isEqualTo(4000000L); + assertThat(window.firstPeriodIndex).isEqualTo(4); + assertThat(window.lastPeriodIndex).isEqualTo(5); + assertThat(window.positionInFirstPeriodUs).isEqualTo(500L); + } + + @Test + public void getPeriodCount_producesExpectedOutput() { + HashMap mediaItemInfos = new HashMap<>(); + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); + mediaItemInfos.put( + asUUID(2), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos, + new DefaultShuffleOrder(5)); + + assertThat(timeline.getPeriodCount()).isEqualTo(6); + } + + @Test + public void getPeriod_forPopulatedPeriod_producesExpectedOutput() { + HashMap mediaItemInfos = new HashMap<>(); + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); + mediaItemInfos.put( + asUUID(5), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos, + new DefaultShuffleOrder(5)); + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ 5, new Timeline.Period(), /* setIds= */ true); + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 5); + + assertThat(period.durationUs).isEqualTo(5000000L); + assertThat(period.windowIndex).isEqualTo(4); + assertThat(period.id).isEqualTo("id2"); + assertThat(period.uid).isEqualTo(periodUid); + } + + @Test + public void getPeriod_forEmptyPeriod_producesExpectedOutput() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + Timeline.Period period = timeline.getPeriod(2, new Timeline.Period(), /* setIds= */ true); + Object uid = timeline.getUidOfPeriod(/* periodIndex= */ 2); + + assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(period.windowIndex).isEqualTo(2); + assertThat(period.id).isEqualTo(MediaItemInfo.EMPTY.periods.get(0).id); + assertThat(period.uid).isEqualTo(uid); + } + + @Test + public void getIndexOfPeriod_worksAcrossDifferentTimelines() { + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + + HashMap mediaItemInfos1 = new HashMap<>(); + mediaItemInfos1.put( + asUUID(1), + new MediaItemInfo( + /* windowDurationUs= */ 5000000L, + /* defaultStartPositionUs= */ 20L, + Collections.singletonList(period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline1 = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2), mediaItemInfos1, new DefaultShuffleOrder(2)); + + HashMap mediaItemInfos2 = new HashMap<>(); + mediaItemInfos2.put( + asUUID(1), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline2 = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem2, mediaItem1, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos2, + new DefaultShuffleOrder(5)); + Object uidOfFirstPeriod = timeline1.getUidOfPeriod(0); + + assertThat(timeline1.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(0); + assertThat(timeline2.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(2); + } + + @Test + public void getIndexOfPeriod_forLastPeriod_producesExpectedOutput() { + MediaItemInfo.Period period1 = + new MediaItemInfo.Period( + "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); + MediaItemInfo.Period period2 = + new MediaItemInfo.Period( + "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); + + HashMap mediaItemInfos1 = new HashMap<>(); + mediaItemInfos1.put( + asUUID(5), + new MediaItemInfo( + /* windowDurationUs= */ 4000000L, + /* defaultStartPositionUs= */ 20L, + Collections.singletonList(period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline singlePeriodTimeline = + ExoCastTimeline.createTimelineFor( + Collections.singletonList(mediaItem5), mediaItemInfos1, new DefaultShuffleOrder(1)); + Object periodUid = singlePeriodTimeline.getUidOfPeriod(0); + + HashMap mediaItemInfos2 = new HashMap<>(); + mediaItemInfos2.put( + asUUID(5), + new MediaItemInfo( + /* windowDurationUs= */ 7000000L, + /* defaultStartPositionUs= */ 20L, + Arrays.asList(period1, period2), + /* positionInFirstPeriodUs= */ 0L, + /* isSeekable= */ true, + /* isDynamic= */ false)); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + mediaItemInfos2, + new DefaultShuffleOrder(5)); + + assertThat(timeline.getIndexOfPeriod(periodUid)).isEqualTo(5); + } + + @Test + public void getUidOfPeriod_withInvalidUid_returnsUnsetIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(/* length= */ 5)); + + assertThat(timeline.getIndexOfPeriod(new Object())).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void getFirstWindowIndex_returnsIndexAccordingToShuffleMode() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(1); + } + + @Test + public void getLastWindowIndex_returnsIndexAccordingToShuffleMode() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(4); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(3); + } + + @Test + public void getNextWindowIndex_repeatModeOne_returnsSameIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(5)); + + for (int i = 0; i < 5; i++) { + assertThat( + timeline.getNextWindowIndex( + i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) + .isEqualTo(i); + assertThat( + timeline.getNextWindowIndex( + i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) + .isEqualTo(i); + } + } + + @Test + public void getNextWindowIndex_onLastIndex_returnsExpectedIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + // Shuffle mode disabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) + .isEqualTo(0); + // Shuffle mode enabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 3, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 3, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) + .isEqualTo(1); + } + + @Test + public void getNextWindowIndex_inMiddleOfQueue_returnsNextIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + // Shuffle mode disabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) + .isEqualTo(3); + // Shuffle mode enabled: + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(0); + } + + @Test + public void getPreviousWindowIndex_repeatModeOne_returnsSameIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + for (int i = 0; i < 5; i++) { + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) + .isEqualTo(i); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) + .isEqualTo(i); + } + } + + @Test + public void getPreviousWindowIndex_onFirstIndex_returnsExpectedIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + // Shuffle mode disabled: + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 0, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) + .isEqualTo(4); + // Shuffle mode enabled: + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 1, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(C.INDEX_UNSET); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 1, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) + .isEqualTo(3); + } + + @Test + public void getPreviousWindowIndex_inMiddleOfQueue_returnsPreviousIndex() { + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), + Collections.emptyMap(), + new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); + + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) + .isEqualTo(3); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(0); + } + + private static UUID asUUID(long number) { + return new UUID(/* mostSigBits= */ 0L, number); + } +} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java new file mode 100644 index 0000000000..fbe936a016 --- /dev/null +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java @@ -0,0 +1,378 @@ +/* + * 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; + +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; +import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; +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.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.UUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link ReceiverAppStateUpdate}. */ +@RunWith(AndroidJUnit4.class) +public class ReceiverAppStateUpdateTest { + + private static final long MOCK_SEQUENCE_NUMBER = 1; + + @Test + public void statusUpdate_withPlayWhenReady_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setPlayWhenReady(true).build(); + JSONObject stateMessage = createStateMessage().put(KEY_PLAY_WHEN_READY, true); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withPlaybackState_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackState(Player.STATE_BUFFERING) + .build(); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_STATE, STR_STATE_BUFFERING); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withMediaQueue_producesExpectedUpdate() throws JSONException { + HashMap requestHeaders = new HashMap<>(); + requestHeaders.put("key", "value"); + MediaItem.UriBundle media = new MediaItem.UriBundle(Uri.parse("www.media.com"), requestHeaders); + MediaItem.DrmScheme drmScheme1 = + new MediaItem.DrmScheme( + C.WIDEVINE_UUID, + new MediaItem.UriBundle(Uri.parse("www.widevine.com"), requestHeaders)); + MediaItem.DrmScheme drmScheme2 = + new MediaItem.DrmScheme( + C.PLAYREADY_UUID, + new MediaItem.UriBundle(Uri.parse("www.playready.com"), requestHeaders)); + MediaItem item = + new MediaItem.Builder() + .setTitle("title") + .setDescription("description") + .setMedia(media) + .setDrmSchemes(Arrays.asList(drmScheme1, drmScheme2)) + .setStartPositionUs(10) + .setEndPositionUs(20) + .build(); + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setItems(Collections.singletonList(item)) + .build(); + JSONObject object = + createStateMessage() + .put(KEY_MEDIA_QUEUE, new JSONArray().put(ExoCastMessage.mediaItemAsJsonObject(item))); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withRepeatMode_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + JSONObject stateMessage = createStateMessage().put(KEY_REPEAT_MODE, STR_REPEAT_MODE_OFF); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withShuffleModeEnabled_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setShuffleModeEnabled(false).build(); + JSONObject stateMessage = createStateMessage().put(KEY_SHUFFLE_MODE_ENABLED, false); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withIsLoading_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setIsLoading(true).build(); + JSONObject stateMessage = createStateMessage().put(KEY_IS_LOADING, true); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withPlaybackParameters_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackParameters( + new PlaybackParameters( + /* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ false)) + .build(); + JSONObject playbackParamsJson = + new JSONObject().put(KEY_SPEED, .5).put(KEY_PITCH, .25).put(KEY_SKIP_SILENCE, false); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_PARAMETERS, playbackParamsJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withTrackSelectionParameters_producesExpectedUpdate() + throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setTrackSelectionParameters( + TrackSelectionParameters.DEFAULT + .buildUpon() + .setDisabledTextTrackSelectionFlags( + C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT) + .setPreferredAudioLanguage("esp") + .setPreferredTextLanguage("deu") + .setSelectUndeterminedTextLanguage(true) + .build()) + .build(); + + JSONArray selectionFlagsJson = + new JSONArray() + .put(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT) + .put(STR_SELECTION_FLAG_FORCED); + JSONObject playbackParamsJson = + new JSONObject() + .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, selectionFlagsJson) + .put(KEY_PREFERRED_AUDIO_LANGUAGE, "esp") + .put(KEY_PREFERRED_TEXT_LANGUAGE, "deu") + .put(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, true); + JSONObject object = + createStateMessage().put(KEY_TRACK_SELECTION_PARAMETERS, playbackParamsJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withError_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setErrorMessage("error message") + .build(); + JSONObject stateMessage = createStateMessage().put(KEY_ERROR_MESSAGE, "error message"); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withPlaybackPosition_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackPosition( + new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) + .build(); + JSONObject positionJson = + new JSONObject() + .put(KEY_UUID, new UUID(0, 1)) + .put(KEY_POSITION_MS, 10) + .put(KEY_PERIOD_ID, "period"); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withDiscontinuity_producesExpectedUpdate() throws JSONException { + ReceiverAppStateUpdate stateUpdate = + ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) + .setPlaybackPosition( + new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) + .build(); + JSONObject positionJson = + new JSONObject() + .put(KEY_UUID, new UUID(0, 1)) + .put(KEY_POSITION_MS, 10) + .put(KEY_PERIOD_ID, "period") + .put(KEY_DISCONTINUITY_REASON, STR_DISCONTINUITY_REASON_SEEK); + JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); + + assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) + .isEqualTo(stateUpdate); + } + + @Test + public void statusUpdate_withMediaItemInfo_producesExpectedTimeline() throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem item1 = builder.setUuid(new UUID(0, 1)).build(); + MediaItem item2 = builder.setUuid(new UUID(0, 2)).build(); + + JSONArray periodsJson = new JSONArray(); + periodsJson + .put(new JSONObject().put(KEY_ID, "id1").put(KEY_DURATION_US, 5000000L)) + .put(new JSONObject().put(KEY_ID, "id2").put(KEY_DURATION_US, 7000000L)) + .put(new JSONObject().put(KEY_ID, "id3").put(KEY_DURATION_US, 6000000L)); + JSONObject mediaItemInfoForUuid1 = new JSONObject(); + mediaItemInfoForUuid1 + .put(KEY_WINDOW_DURATION_US, 10000000L) + .put(KEY_DEFAULT_START_POSITION_US, 1000000L) + .put(KEY_PERIODS, periodsJson) + .put(KEY_POSITION_IN_FIRST_PERIOD_US, 2000000L) + .put(KEY_IS_DYNAMIC, false) + .put(KEY_IS_SEEKABLE, true); + JSONObject mediaItemInfoMapJson = + new JSONObject().put(new UUID(0, 1).toString(), mediaItemInfoForUuid1); + + JSONObject receiverAppStateUpdateJson = + createStateMessage().put(KEY_MEDIA_ITEMS_INFO, mediaItemInfoMapJson); + ReceiverAppStateUpdate receiverAppStateUpdate = + ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + Arrays.asList(item1, item2), + receiverAppStateUpdate.mediaItemsInformation, + new ShuffleOrder.DefaultShuffleOrder( + /* shuffledIndices= */ new int[] {1, 0}, /* randomSeed= */ 0)); + Timeline.Window window0 = + timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window(), /* setTag= */ true); + Timeline.Window window1 = + timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window(), /* setTag= */ true); + Timeline.Period[] periods = new Timeline.Period[4]; + for (int i = 0; i < 4; i++) { + periods[i] = + timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true); + } + + assertThat(timeline.getWindowCount()).isEqualTo(2); + assertThat(window0.positionInFirstPeriodUs).isEqualTo(2000000L); + assertThat(window0.durationUs).isEqualTo(10000000L); + assertThat(window0.isDynamic).isFalse(); + assertThat(window0.isSeekable).isTrue(); + assertThat(window0.defaultPositionUs).isEqualTo(1000000L); + assertThat(window1.positionInFirstPeriodUs).isEqualTo(0L); + assertThat(window1.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window1.isDynamic).isTrue(); + assertThat(window1.isSeekable).isFalse(); + assertThat(window1.defaultPositionUs).isEqualTo(0L); + + assertThat(timeline.getPeriodCount()).isEqualTo(4); + assertThat(periods[0].id).isEqualTo("id1"); + assertThat(periods[0].getPositionInWindowUs()).isEqualTo(-2000000L); + assertThat(periods[0].durationUs).isEqualTo(5000000L); + assertThat(periods[1].id).isEqualTo("id2"); + assertThat(periods[1].durationUs).isEqualTo(7000000L); + assertThat(periods[1].getPositionInWindowUs()).isEqualTo(3000000L); + assertThat(periods[2].id).isEqualTo("id3"); + assertThat(periods[2].durationUs).isEqualTo(6000000L); + assertThat(periods[2].getPositionInWindowUs()).isEqualTo(10000000L); + assertThat(periods[3].durationUs).isEqualTo(C.TIME_UNSET); + } + + @Test + public void statusUpdate_withShuffleOrder_producesExpectedTimeline() throws JSONException { + MediaItem.Builder builder = new MediaItem.Builder(); + JSONObject receiverAppStateUpdateJson = + createStateMessage().put(KEY_SHUFFLE_ORDER, new JSONArray(Arrays.asList(2, 3, 1, 0))); + ReceiverAppStateUpdate receiverAppStateUpdate = + ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); + ExoCastTimeline timeline = + ExoCastTimeline.createTimelineFor( + /* mediaItems= */ Arrays.asList( + builder.build(), builder.build(), builder.build(), builder.build()), + /* mediaItemInfoMap= */ Collections.emptyMap(), + /* shuffleOrder= */ new ShuffleOrder.DefaultShuffleOrder( + Util.toArray(receiverAppStateUpdate.shuffleOrder), /* randomSeed= */ 0)); + + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 2, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(3); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 3, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(1); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 1, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(0); + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 0, + /* repeatMode= */ Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(C.INDEX_UNSET); + } + + private static JSONObject createStateMessage() throws JSONException { + return new JSONObject().put(KEY_SEQUENCE_NUMBER, MOCK_SEQUENCE_NUMBER); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index cb548ec3fd..12db27d68e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -71,6 +71,7 @@ public final class DecryptableSampleQueueReader { * @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()); } @@ -179,4 +180,21 @@ public final class DecryptableSampleQueueReader { 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 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/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index b2c09bd70f..542565e70d 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; @@ -214,6 +215,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. * 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 976a5d4e48..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. * From 883b3c8783468e98108581d3d0f284f514c81cde Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 24 Jun 2019 18:13:31 +0100 Subject: [PATCH 141/807] Update isMediaCodecException to return true for generic ISE on API 21+ if the stack trace contains MediaCodec. PiperOrigin-RevId: 254781909 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ef3cb0bbe3..30083cb849 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 @@ -1743,8 +1743,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private static boolean isMediaCodecException(IllegalStateException error) { - if (Util.SDK_INT >= 21) { - return isMediaCodecExceptionV21(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"); From 4e504bc485cfaef7b56deb7da6f7a33ce12b494b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 26 Jun 2019 11:49:08 +0100 Subject: [PATCH 142/807] Rename DeferredMediaPeriod to MaskingMediaPeriod. This better reflects its usage and fits into our general naming pattern. PiperOrigin-RevId: 255157159 --- .../source/ConcatenatingMediaSource.java | 10 ++-- ...diaPeriod.java => MaskingMediaPeriod.java} | 12 ++--- .../exoplayer2/source/ads/AdsMediaSource.java | 46 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/source/{DeferredMediaPeriod.java => MaskingMediaPeriod.java} (94%) 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..c031fcde21 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 @@ -456,8 +456,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; + public final List activeMediaPeriods; public DeferredTimeline timeline; public int childIndex; 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 94% 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 95a218bfe7..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 @@ -32,7 +32,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * #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 { @@ -45,7 +45,7 @@ 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; @@ -58,14 +58,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb 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; @@ -85,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; } 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 78b0f6de11..dd4c0d26b2 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; @@ -128,7 +128,7 @@ 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. @@ -179,7 +179,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][]; @@ -219,29 +219,29 @@ public final class AdsMediaSource extends CompositeMediaSource { if (mediaSource == null) { mediaSource = adMediaSourceFactory.createMediaSource(adUri); adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; - deferredMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); + maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); prepareChildSource(id, mediaSource); } - DeferredMediaPeriod deferredMediaPeriod = - new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); - deferredMediaPeriod.setPrepareErrorListener( + 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 = 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; } @@ -249,13 +249,13 @@ 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 @@ -263,7 +263,7 @@ public final class AdsMediaSource extends CompositeMediaSource { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; - deferredMediaPeriodByAdMediaSource.clear(); + maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; contentManifest = null; adPlaybackState = null; @@ -319,11 +319,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); @@ -413,7 +413,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; From 40d44c48e59c31da8abec4492dbe96ba68152471 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 26 Jun 2019 15:16:22 +0100 Subject: [PATCH 143/807] Add threading model note to hello-word page Also add layer of indirection between code and the guide, to make moving content easier going forward. PiperOrigin-RevId: 255182216 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 da66f3dd10..1c6458c551 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 @@ -1233,8 +1233,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; } From 8faac0344b4340fbd34e623620da15575a67a4f7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Jun 2019 12:08:23 +0100 Subject: [PATCH 144/807] Fix checkerframework 2.8.2 warnings. The updated version issues more warnings than before. Most of the changes are related to annotation placement. PiperOrigin-RevId: 255371743 --- .../ext/cast/ReceiverAppStateUpdate.java | 28 +++++++++---------- .../exoplayer2/ext/gvr/GvrPlayerActivity.java | 12 ++++---- .../DefaultPlaybackSessionManager.java | 2 +- .../exoplayer2/analytics/PlaybackStats.java | 3 +- .../analytics/PlaybackStatsListener.java | 15 +++++----- .../source/DecryptableSampleQueueReader.java | 2 +- .../upstream/ByteArrayDataSink.java | 2 +- .../upstream/DefaultBandwidthMeter.java | 2 +- .../cache/CacheFileMetadataIndex.java | 2 +- .../upstream/cache/SimpleCache.java | 2 +- .../exoplayer2/ui/TrackSelectionView.java | 2 +- .../ui/spherical/CanvasRenderer.java | 4 +-- .../ui/spherical/ProjectionRenderer.java | 2 +- 13 files changed, 39 insertions(+), 39 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java index 8cb6056340..c1b12428d4 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java @@ -110,20 +110,20 @@ public final class ReceiverAppStateUpdate { public static final class Builder { private final long sequenceNumber; - @MonotonicNonNull private Boolean playWhenReady; - @MonotonicNonNull private Integer playbackState; - @MonotonicNonNull private List items; - @MonotonicNonNull private Integer repeatMode; - @MonotonicNonNull private Boolean shuffleModeEnabled; - @MonotonicNonNull private Boolean isLoading; - @MonotonicNonNull private PlaybackParameters playbackParameters; - @MonotonicNonNull private TrackSelectionParameters trackSelectionParameters; - @MonotonicNonNull private String errorMessage; - @MonotonicNonNull private Integer discontinuityReason; - @MonotonicNonNull private UUID currentPlayingItemUuid; - @MonotonicNonNull private String currentPlayingPeriodId; - @MonotonicNonNull private Long currentPlaybackPositionMs; - @MonotonicNonNull private List shuffleOrder; + private @MonotonicNonNull Boolean playWhenReady; + private @MonotonicNonNull Integer playbackState; + private @MonotonicNonNull List items; + private @MonotonicNonNull Integer repeatMode; + private @MonotonicNonNull Boolean shuffleModeEnabled; + private @MonotonicNonNull Boolean isLoading; + private @MonotonicNonNull PlaybackParameters playbackParameters; + private @MonotonicNonNull TrackSelectionParameters trackSelectionParameters; + private @MonotonicNonNull String errorMessage; + private @MonotonicNonNull Integer discontinuityReason; + private @MonotonicNonNull UUID currentPlayingItemUuid; + private @MonotonicNonNull String currentPlayingPeriodId; + private @MonotonicNonNull Long currentPlaybackPositionMs; + private @MonotonicNonNull List shuffleOrder; private Map mediaItemsInformation; private Builder(long sequenceNumber) { 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 38fa3a36e5..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 @@ -61,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/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 index f633bfbf8e..ed127bc550 100644 --- 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 @@ -502,8 +502,7 @@ public final class PlaybackStats { * @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. */ - @PlaybackState - public int getPlaybackStateAtTime(long realtimeMs) { + public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; for (Pair timeAndState : playbackStateHistory) { if (timeAndState.first.realtimeMs > realtimeMs) { 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 index 8c58133704..6444b4747f 100644 --- 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 @@ -452,7 +452,7 @@ public final class PlaybackStatsListener private int nonFatalErrorCount; // Current player state tracking. - @PlaybackState private int currentPlaybackState; + private @PlaybackState int currentPlaybackState; private long currentPlaybackStateStartTimeMs; private boolean isSeeking; private boolean isForeground; @@ -713,8 +713,6 @@ public final class PlaybackStatsListener * * @param isFinal Whether this is the final build and no further events are expected. */ - // TODO(b/133387873): incompatible types in conditional expression. - @SuppressWarnings("nullness:conditional.type.incompatible") public PlaybackStats build(boolean isFinal) { long[] playbackStateDurationsMs = this.playbackStateDurationsMs; List mediaTimeHistory = this.mediaTimeHistory; @@ -739,6 +737,10 @@ public final class PlaybackStatsListener : 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, @@ -757,8 +759,8 @@ public final class PlaybackStatsListener rebufferCount, maxRebufferTimeMs, /* adPlaybackCount= */ isAd ? 1 : 0, - isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory), - isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory), + videoHistory, + audioHistory, videoFormatHeightTimeMs, videoFormatHeightTimeProduct, videoFormatBitrateTimeMs, @@ -826,8 +828,7 @@ public final class PlaybackStatsListener } } - @PlaybackState - private int resolveNewPlaybackState() { + 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index 12db27d68e..4f0c5b87aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -39,7 +39,7 @@ public final class DecryptableSampleQueueReader { private final DrmSessionManager sessionManager; private final FormatHolder formatHolder; private final boolean playClearSamplesWithoutKeys; - @MonotonicNonNull private Format currentFormat; + private @MonotonicNonNull Format currentFormat; @Nullable private DrmSession currentSession; /** 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 a9f9da0a95..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 @@ -29,7 +29,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public final class ByteArrayDataSink implements DataSink { - @MonotonicNonNull private ByteArrayOutputStream stream; + private @MonotonicNonNull ByteArrayOutputStream stream; @Override public void open(DataSpec dataSpec) { 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/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/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 1d4481b5cc..ea37612c88 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 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/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 8a211d0879..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 From 3c2afb16e63ea71155da39f5647aa227c6b36f26 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 12:59:56 +0100 Subject: [PATCH 145/807] Cleanup: Remove deprecated ChunkSampleStream constructor PiperOrigin-RevId: 255377347 --- .../source/chunk/ChunkSampleStream.java | 41 ------------------- 1 file changed, 41 deletions(-) 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 499aea6a0c..efc3b47596 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 @@ -26,7 +26,6 @@ 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; @@ -84,46 +83,6 @@ public class ChunkSampleStream implements SampleStream, S /* package */ long decodeOnlyUntilPositionUs; /* package */ boolean loadingFinished; - /** - * 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 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, - @Nullable int[] embeddedTrackTypes, - @Nullable 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. * From c974f74b1f626874c92dbe0ad4fdc6cbcc865eed Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:04:32 +0100 Subject: [PATCH 146/807] Cleanup: Remove deprecated DataSpec.postBody PiperOrigin-RevId: 255378274 --- .../com/google/android/exoplayer2/upstream/DataSpec.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 99a3d271bd..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,13 +95,11 @@ 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. */ @Nullable public final byte[] httpBody; - /** @deprecated Use {@link #httpBody} instead. */ - @Deprecated @Nullable public final byte[] postBody; - /** * The absolute position of the data in the full stream. */ @@ -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; From 2a366e76b778a01e9c58844a3caaf90275b582d4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:14:00 +0100 Subject: [PATCH 147/807] Cleanup: Remove deprecated message sending functionality PiperOrigin-RevId: 255379393 --- .../google/android/exoplayer2/ExoPlayer.java | 37 ----------------- .../android/exoplayer2/ExoPlayerImpl.java | 41 ------------------- .../android/exoplayer2/SimpleExoPlayer.java | 14 ------- .../exoplayer2/testutil/StubExoPlayer.java | 14 ------- 4 files changed, 106 deletions(-) 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/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index c458f22050..945bd32d30 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}. */ @@ -410,15 +408,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( @@ -429,36 +418,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()) { 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 1c6458c551..b427991d6e 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 @@ -1034,26 +1034,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(); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 56de0a8b33..df96b634dd 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/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(); From 1d36edc2148234cd8023fefd9b7620e7a7d5c76b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:26:47 +0100 Subject: [PATCH 148/807] Remove unnecessary FileDescriptor sync PiperOrigin-RevId: 255380796 --- .../exoplayer2/upstream/cache/CacheDataSink.java | 16 ---------------- .../upstream/cache/CacheDataSinkFactory.java | 16 +--------------- 2 files changed, 1 insertion(+), 31 deletions(-) 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); } } From cf68d4eb474d8ece404e6e0d920826832eeb0a28 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:28:18 +0100 Subject: [PATCH 149/807] Cleanup: Remove deprecated text and metadata output interfaces PiperOrigin-RevId: 255380951 --- .../android/exoplayer2/metadata/MetadataRenderer.java | 6 ------ .../com/google/android/exoplayer2/text/TextRenderer.java | 6 ------ 2 files changed, 12 deletions(-) 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 a72c70442e..a775481633 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 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 bdf127be59..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({ From 1bd73eb70ed4341acb9dfd815665ddf683100e57 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Jun 2019 16:52:57 +0100 Subject: [PATCH 150/807] Cleanup: Remove DynamicConcatenatingMediaSource PiperOrigin-RevId: 255410268 --- .../source/ConcatenatingMediaSource.java | 57 +++++++++---------- .../DynamicConcatenatingMediaSource.java | 46 --------------- 2 files changed, 28 insertions(+), 75 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java 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 c031fcde21..c2ec437d84 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 @@ -46,7 +46,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; @@ -149,7 +149,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(Collection mediaSources) { addPublicMediaSources( mediaSourcesPublic.size(), mediaSources, @@ -221,7 +221,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, Runnable onCompletionAction) { addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } @@ -234,7 +234,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(int index, Collection mediaSources) { addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } @@ -249,7 +249,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, @@ -269,7 +269,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource Date: Thu, 27 Jun 2019 17:03:54 +0100 Subject: [PATCH 151/807] Visibility clean-up: Don't extend visibility of protected methods in overrides. PiperOrigin-RevId: 255412493 --- .../google/android/exoplayer2/source/ClippingMediaSource.java | 4 ++-- .../android/exoplayer2/source/CompositeMediaSource.java | 4 ++-- .../android/exoplayer2/source/ExtractorMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/LoopingMediaSource.java | 2 +- .../google/android/exoplayer2/source/MergingMediaSource.java | 4 ++-- .../android/exoplayer2/source/ProgressiveMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/SilenceMediaSource.java | 4 ++-- .../android/exoplayer2/source/SingleSampleMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/ads/AdsMediaSource.java | 4 ++-- .../android/exoplayer2/source/dash/DashMediaSource.java | 4 ++-- .../google/android/exoplayer2/source/hls/HlsMediaSource.java | 4 ++-- .../exoplayer2/source/smoothstreaming/SsMediaSource.java | 4 ++-- .../google/android/exoplayer2/testutil/FakeMediaSource.java | 2 +- 13 files changed, 24 insertions(+), 24 deletions(-) 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 c3e700fff5..c942f9320e 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 @@ -193,7 +193,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); } @@ -228,7 +228,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); clippingError = null; clippingTimeline = null; 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 06db088f06..1a9e1ff250 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 @@ -44,7 +44,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; eventHandler = new Handler(); } @@ -59,7 +59,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void releaseSourceInternal() { + protected void releaseSourceInternal() { for (MediaSourceAndListener childSource : childSources.values()) { childSource.mediaSource.releaseSource(childSource.listener); childSource.mediaSource.removeEventListener(childSource.eventListener); 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 d9003e443e..f07ee63e79 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 @@ -339,7 +339,7 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); } @@ -359,7 +359,7 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { progressiveMediaSource.releaseSource(/* listener= */ this); } 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..7adb18dc94 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); } 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 7188cada0f..f12ce92f54 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 @@ -104,7 +104,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,7 +140,7 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); Arrays.fill(timelines, null); primaryManifest = null; 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..ba69b46d7f 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 @@ -228,7 +228,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); } @@ -262,7 +262,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { // Do nothing. } 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 index b03dd0ea7c..fc99e8cb7b 100644 --- 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 @@ -66,7 +66,7 @@ public final class SilenceMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), /* manifest= */ null); @@ -84,7 +84,7 @@ public final class SilenceMediaSource extends BaseMediaSource { public void releasePeriod(MediaPeriod mediaPeriod) {} @Override - public void releaseSourceInternal() {} + protected void releaseSourceInternal() {} private static final class SilenceMediaPeriod implements MediaPeriod { 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 55d967cd69..6c1881a01a 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 @@ -302,7 +302,7 @@ 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); } @@ -331,7 +331,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/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index dd4c0d26b2..a6c2cf2767 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 @@ -193,7 +193,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; @@ -259,7 +259,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; 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 779a97fd09..551555502f 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 @@ -630,7 +630,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); @@ -679,7 +679,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifestLoadPending = false; dataSource = null; if (loader != null) { 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 fbb6285d1d..f891670e78 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 @@ -333,7 +333,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); @@ -366,7 +366,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { playlistTracker.stop(); } 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 3c18bfe644..e31fbccae5 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 @@ -525,7 +525,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(); @@ -568,7 +568,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifest = sideloadedManifest ? manifest : null; manifestDataSource = null; manifestLoadStartTimestamp = 0; 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 0d50f22bc0..80456169ff 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 @@ -137,7 +137,7 @@ public class FakeMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); assertThat(activeMediaPeriods.isEmpty()).isTrue(); From 244c202c5667600e1e6613da426f01018ceb20a4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 27 Jun 2019 19:26:56 +0100 Subject: [PATCH 152/807] Fix hidden API warnings from Metalava PiperOrigin-RevId: 255442455 --- .../exoplayer2/drm/DefaultDrmSession.java | 2 +- .../exoplayer2/extractor/ts/H262Reader.java | 2 +- .../exoplayer2/extractor/ts/SeiReader.java | 6 ++-- .../exoplayer2/text/dvb/DvbDecoder.java | 3 +- .../exoplayer2/text/ssa/SsaDecoder.java | 3 +- .../exoplayer2/text/subrip/SubripDecoder.java | 9 +++--- .../exoplayer2/text/ttml/TtmlDecoder.java | 3 +- .../text/webvtt/Mp4WebvttDecoder.java | 3 +- .../exoplayer2/text/webvtt/WebvttDecoder.java | 3 +- .../upstream/cache/SimpleCache.java | 6 ++-- .../exoplayer2/text/ssa/SsaDecoderTest.java | 17 ++++++----- .../text/subrip/SubripDecoderTest.java | 29 ++++++++++--------- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 2 +- .../text/webvtt/WebvttDecoderTest.java | 2 +- .../upstream/cache/CacheDataSourceTest.java | 2 +- .../upstream/cache/SimpleCacheTest.java | 2 +- 16 files changed, 50 insertions(+), 44 deletions(-) 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 e49602957f..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 @@ -45,7 +45,7 @@ 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 { 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/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/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/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c25b26128c..d701f99d73 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 @@ -19,6 +19,7 @@ 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; @@ -72,7 +73,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(); 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..cf174283ec 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); 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 6e0c495466..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(); 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 b977f61a8a..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; @@ -49,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 fe3c86bd1e..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. 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 ea37612c88..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 @@ -380,13 +380,13 @@ 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 { @@ -402,7 +402,7 @@ 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(); 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 85af6482c0..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 @@ -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/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/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/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(); From ae0aeb046b11cc21a2f8470f5eb2f6cb211a0e1e Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 27 Jun 2019 21:51:41 +0100 Subject: [PATCH 153/807] call setPlayWhenReady in any case ISSUE: #6093 PiperOrigin-RevId: 255471282 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9ec3886df5..eaebf8b4e1 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 @@ -1089,8 +1089,9 @@ public final class MediaSessionConnector { } } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } + controlDispatcher.dispatchSetPlayWhenReady( + Assertions.checkNotNull(player), /* playWhenReady= */ true); } } From 6fe70ca43d982ad51ee30b7afbb26ca079b19c84 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 28 Jun 2019 13:21:43 +0100 Subject: [PATCH 154/807] Use the floor of the frame rate for capability checks PiperOrigin-RevId: 255584000 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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..3310b0dc8b 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 @@ -520,9 +520,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) From 71de1d37ac58ce28df70c8fa3017b0688c0cf266 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 1 Jul 2019 13:03:07 +0100 Subject: [PATCH 155/807] Don't consume touch events if no controller is attached. Issue:#6109 PiperOrigin-RevId: 255933121 --- .../com/google/android/exoplayer2/ui/PlayerView.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 7e01801daf..269c48c282 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 @@ -1050,6 +1050,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTouchEvent(MotionEvent event) { + if (!useController || player == null) { + return false; + } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isTouching = true; @@ -1150,9 +1153,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { - if (!useController || player == null) { - return false; - } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1471,6 +1471,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onSingleTapUp(MotionEvent e) { + if (!useController || player == null) { + return false; + } return toggleControllerVisibility(); } } From 04959ec648389d6ce71cabf54dc0e8bc1fcfe22d Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 1 Jul 2019 14:49:35 +0100 Subject: [PATCH 156/807] Remove unnecessary variables from ConcatenatingMediaSource. The total window and period count, as well as the period offset for each holder are not actually needed and can be removed. Also added a TODO to remove two other variables if possible. PiperOrigin-RevId: 255945584 --- .../source/ConcatenatingMediaSource.java | 85 +++++++------------ 1 file changed, 31 insertions(+), 54 deletions(-) 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 c2ec437d84..bdf55fe40d 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,7 +18,6 @@ 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; @@ -78,8 +77,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource nextTimelineUpdateOnCompletionActions; private ShuffleOrder shuffleOrder; - private int windowCount; - private int periodCount; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same @@ -483,8 +480,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource onCompletionActions = nextTimelineUpdateOnCompletionActions; nextTimelineUpdateOnCompletionActions = new HashSet<>(); refreshSourceInfo( - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - /* manifest= */ null); + new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic), /* manifest= */ null); getPlaybackThreadHandlerOnPlaybackThread() .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); @@ -737,17 +730,12 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource { + /* package */ static final class MediaSourceHolder { public final MediaSource mediaSource; public final Object uid; @@ -901,7 +883,6 @@ public final 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]; @@ -970,13 +941,19 @@ public final 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; + 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 From 7798c07f64d58a5363ff2af1a72cdcad9a8ae2de Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 1 Jul 2019 16:52:36 +0100 Subject: [PATCH 157/807] Remove ExoCast PiperOrigin-RevId: 255964199 --- cast_receiver_app/BUILD | 310 ---- cast_receiver_app/README.md | 72 - cast_receiver_app/WORKSPACE | 38 - cast_receiver_app/app-desktop/html/index.css | 156 -- cast_receiver_app/app-desktop/html/index.html | 55 - cast_receiver_app/app-desktop/src/main.js | 170 -- .../app-desktop/src/player_controls.js | 164 -- cast_receiver_app/app-desktop/src/samples.js | 70 - .../app-desktop/src/samples_internal.js | 79 - cast_receiver_app/app/html/index.css | 39 - cast_receiver_app/app/html/index.html | 40 - .../app/html/playback_info_view.css | 59 - cast_receiver_app/app/src/main.js | 55 - .../app/src/message_dispatcher.js | 234 --- cast_receiver_app/app/src/receiver.js | 191 --- cast_receiver_app/app/src/validation.js | 163 -- cast_receiver_app/assemble.bazel.sh | 93 - cast_receiver_app/externs/protocol.js | 489 ------ cast_receiver_app/externs/shaka.js | 68 - .../src/configuration_factory.js | 90 - cast_receiver_app/src/constants.js | 140 -- cast_receiver_app/src/playback_info_view.js | 233 --- cast_receiver_app/src/player.js | 1522 ----------------- cast_receiver_app/src/timeout.js | 68 - cast_receiver_app/src/util.js | 62 - cast_receiver_app/test/caf_bootstrap.js | 33 - .../test/configuration_factory_test.js | 86 - cast_receiver_app/test/externs.js | 36 - .../test/message_dispatcher_test.js | 49 - cast_receiver_app/test/mocks.js | 277 --- .../test/playback_info_view_test.js | 242 --- cast_receiver_app/test/player_test.js | 470 ----- cast_receiver_app/test/queue_test.js | 166 -- cast_receiver_app/test/receiver_test.js | 1027 ----------- .../test/shaka_error_handling_test.js | 84 - cast_receiver_app/test/util.js | 87 - cast_receiver_app/test/validation_test.js | 266 --- .../DefaultReceiverPlayerManager.java | 437 ----- .../android/exoplayer2/castdemo/DemoUtil.java | 5 + .../castdemo/ExoCastPlayerManager.java | 421 ----- .../exoplayer2/castdemo/MainActivity.java | 35 +- .../exoplayer2/castdemo/PlayerManager.java | 422 ++++- demos/cast/src/main/res/values/strings.xml | 2 - .../ext/cast/CastSessionManager.java | 86 - .../ext/cast/DefaultCastOptionsProvider.java | 2 +- .../ext/cast/DefaultCastSessionManager.java | 187 -- .../exoplayer2/ext/cast/ExoCastConstants.java | 118 -- .../exoplayer2/ext/cast/ExoCastMessage.java | 474 ----- .../ext/cast/ExoCastOptionsProvider.java | 40 - .../exoplayer2/ext/cast/ExoCastPlayer.java | 958 ----------- .../exoplayer2/ext/cast/ExoCastTimeline.java | 342 ---- .../exoplayer2/ext/cast/MediaItemInfo.java | 160 -- .../exoplayer2/ext/cast/MediaItemQueue.java | 85 - .../ext/cast/ReceiverAppStateUpdate.java | 633 ------- .../ext/cast/ExoCastMessageTest.java | 436 ----- .../ext/cast/ExoCastPlayerTest.java | 1018 ----------- .../ext/cast/ExoCastTimelineTest.java | 466 ----- .../ext/cast/ReceiverAppStateUpdateTest.java | 378 ---- 58 files changed, 407 insertions(+), 13781 deletions(-) delete mode 100644 cast_receiver_app/BUILD delete mode 100644 cast_receiver_app/README.md delete mode 100644 cast_receiver_app/WORKSPACE delete mode 100644 cast_receiver_app/app-desktop/html/index.css delete mode 100644 cast_receiver_app/app-desktop/html/index.html delete mode 100644 cast_receiver_app/app-desktop/src/main.js delete mode 100644 cast_receiver_app/app-desktop/src/player_controls.js delete mode 100644 cast_receiver_app/app-desktop/src/samples.js delete mode 100644 cast_receiver_app/app-desktop/src/samples_internal.js delete mode 100644 cast_receiver_app/app/html/index.css delete mode 100644 cast_receiver_app/app/html/index.html delete mode 100644 cast_receiver_app/app/html/playback_info_view.css delete mode 100644 cast_receiver_app/app/src/main.js delete mode 100644 cast_receiver_app/app/src/message_dispatcher.js delete mode 100644 cast_receiver_app/app/src/receiver.js delete mode 100644 cast_receiver_app/app/src/validation.js delete mode 100755 cast_receiver_app/assemble.bazel.sh delete mode 100644 cast_receiver_app/externs/protocol.js delete mode 100644 cast_receiver_app/externs/shaka.js delete mode 100644 cast_receiver_app/src/configuration_factory.js delete mode 100644 cast_receiver_app/src/constants.js delete mode 100644 cast_receiver_app/src/playback_info_view.js delete mode 100644 cast_receiver_app/src/player.js delete mode 100644 cast_receiver_app/src/timeout.js delete mode 100644 cast_receiver_app/src/util.js delete mode 100644 cast_receiver_app/test/caf_bootstrap.js delete mode 100644 cast_receiver_app/test/configuration_factory_test.js delete mode 100644 cast_receiver_app/test/externs.js delete mode 100644 cast_receiver_app/test/message_dispatcher_test.js delete mode 100644 cast_receiver_app/test/mocks.js delete mode 100644 cast_receiver_app/test/playback_info_view_test.js delete mode 100644 cast_receiver_app/test/player_test.js delete mode 100644 cast_receiver_app/test/queue_test.js delete mode 100644 cast_receiver_app/test/receiver_test.js delete mode 100644 cast_receiver_app/test/shaka_error_handling_test.js delete mode 100644 cast_receiver_app/test/util.js delete mode 100644 cast_receiver_app/test/validation_test.js delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java delete mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java diff --git a/cast_receiver_app/BUILD b/cast_receiver_app/BUILD deleted file mode 100644 index 2bd0526cdd..0000000000 --- a/cast_receiver_app/BUILD +++ /dev/null @@ -1,310 +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. - -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_test") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_css_library") -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_css_binary") - -licenses(["notice"]) # Apache 2.0 - -# The Shaka player library - 2.5.0-beta2 (needs to be cloned from Github). -closure_js_library( - name = "shaka_player_library", - srcs = glob( - [ - "external-js/shaka-player/lib/**/*.js", - "external-js/shaka-player/externs/**/*.js", - ], - exclude = [ - "external-js/shaka-player/lib/debug/asserts.js", - "external-js/shaka-player/externs/mediakeys.js", - "external-js/shaka-player/externs/networkinformation.js", - "external-js/shaka-player/externs/vtt_region.js", - ], - ), - suppress = [ - "strictMissingRequire", - "missingSourcesWarnings", - "analyzerChecks", - "strictCheckTypes", - "checkTypes", - ], - deps = [ - "@io_bazel_rules_closure//closure/library", - ], -) - -# The plain player not depending on the cast library. -closure_js_library( - name = "player_lib", - srcs = [ - "externs/protocol.js", - "src/configuration_factory.js", - "src/constants.js", - "src/playback_info_view.js", - "src/player.js", - "src/timeout.js", - "src/util.js", - ], - suppress = [ - "missingSourcesWarnings", - "analyzerChecks", - "strictCheckTypes", - ], - deps = [ - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - ], -) - -# A debug app to test the player with a desktop browser. -closure_js_library( - name = "app_desktop_lib", - srcs = [ - "app-desktop/src/main.js", - "app-desktop/src/player_controls.js", - "app-desktop/src/samples.js", - "externs/shaka.js", - ], - suppress = [ - "reportUnknownTypes", - "strictCheckTypes", - ], - deps = [ - ":player_lib", - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - ], -) - -# Includes the javascript files of the cast receiver app. -closure_js_library( - name = "app_lib", - srcs = [ - "app/src/main.js", - "app/src/message_dispatcher.js", - "app/src/receiver.js", - "app/src/validation.js", - "externs/cast.js", - "externs/shaka.js", - ], - suppress = [ - "missingSourcesWarnings", - "analyzerChecks", - "strictCheckTypes", - ], - deps = [ - ":player_lib", - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - ], -) - -# Test utils like mocks. -closure_js_library( - name = "test_util_lib", - testonly = 1, - srcs = [ - "externs/protocol.js", - "test/externs.js", - "test/mocks.js", - "test/util.js", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":shaka_player_library", - "@io_bazel_rules_closure//closure/library", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - ], -) - -# Unit test for the player. -closure_js_test( - name = "player_tests", - srcs = glob([ - "test/player_test.js", - ]), - entry_points = [ - "exoplayer.cast.test", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# Unit test for the queue in the player. -closure_js_test( - name = "queue_tests", - srcs = glob([ - "test/queue_test.js", - ]), - entry_points = [ - "exoplayer.cast.test.queue", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# Unit test for the receiver. -closure_js_test( - name = "receiver_tests", - srcs = glob([ - "test/receiver_test.js", - ]), - entry_points = [ - "exoplayer.cast.test.receiver", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# Unit test for the validations. -closure_js_test( - name = "validation_tests", - srcs = [ - "test/validation_test.js", - ], - entry_points = [ - "exoplayer.cast.test.validation", - ], - suppress = [ - "checkTypes", - "strictCheckTypes", - "reportUnknownTypes", - "accessControls", - "analyzerChecks", - "missingSourcesWarnings", - ], - deps = [ - ":app_lib", - ":player_lib", - ":test_util_lib", - "@io_bazel_rules_closure//closure/library/testing:asserts", - "@io_bazel_rules_closure//closure/library/testing:jsunit", - "@io_bazel_rules_closure//closure/library/testing:testsuite", - ], -) - -# The receiver app as a compiled binary. -closure_js_binary( - name = "app", - entry_points = [ - "exoplayer.cast.app", - "shaka.dash.DashParser", - "shaka.hls.HlsParser", - "shaka.abr.SimpleAbrManager", - "shaka.net.HttpFetchPlugin", - "shaka.net.HttpXHRPlugin", - "shaka.media.AdaptationSetCriteria", - ], - deps = [":app_lib"], -) - -# The debug app for the player as a compiled binary. -closure_js_binary( - name = "app_desktop", - entry_points = [ - "exoplayer.cast.debug", - "exoplayer.cast.samples", - "shaka.dash.DashParser", - "shaka.hls.HlsParser", - "shaka.abr.SimpleAbrManager", - "shaka.net.HttpFetchPlugin", - "shaka.net.HttpXHRPlugin", - "shaka.media.AdaptationSetCriteria", - ], - deps = [":app_desktop_lib"], -) - -# Defines the css style of the receiver app. -closure_css_library( - name = "app_styles_lib", - srcs = [ - "app/html/index.css", - "app/html/playback_info_view.css", - ], -) - -# Defines the css styles of the debug app. -closure_css_library( - name = "app_desktop_styles_lib", - srcs = [ - "app-desktop/html/index.css", - "app/html/playback_info_view.css", - ], -) - -# Compiles the css styles of the receiver app. -closure_css_binary( - name = "app_styles", - renaming = False, - deps = ["app_styles_lib"], -) - -# Compiles the css styles of the debug app. -closure_css_binary( - name = "app_desktop_styles", - renaming = False, - deps = ["app_desktop_styles_lib"], -) diff --git a/cast_receiver_app/README.md b/cast_receiver_app/README.md deleted file mode 100644 index 6504cb4f94..0000000000 --- a/cast_receiver_app/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# ExoPlayer cast receiver # - -An HTML/JavaScript app which runs within a Google cast device and can be loaded -and controller by an Android app which uses the ExoPlayer cast extension -(https://github.com/google/ExoPlayer/tree/release-v2/extensions/cast). - -# Build the app # - -You can build and deploy the app to your web server and register the url as your -cast receiver app (see: https://developers.google.com/cast/docs/registration). - -Building the app compiles JavaScript and CSS files. Dead JavaScript code of the -app itself and their dependencies (like ShakaPlayer) is removed and the -remaining code is minimized. - -## Prerequisites ## - -1. Install the most recent bazel release (https://bazel.build/) which is at - least 0.22.0. - -From within the root of the exo_receiver_app project do the following steps: - -2. Clone shaka from GitHub into the directory external-js/shaka-player: -``` -# git clone https://github.com/google/shaka-player.git \ - external-js/shaka-player -``` - -## 1. Customize html page and css (optional) ## - -(Optional) Edit index.html. **Make sure you do not change the id of the video -element**. -(Optional) Customize main.css. - -## 2. Build javascript and css files ## -``` -# bazel build ... -``` -## 3. Assemble the receiver app ## -``` -# WEB_DEPLOY_DIR=www -# mkdir ${WEB_DEPLOY_DIR} -# cp bazel-bin/exo_receiver_app.js ${WEB_DEPLOY_DIR} -# cp bazel-bin/exo_receiver_styles_bin.css ${WEB_DEPLOY_DIR} -# cp html/index.html ${WEB_DEPLOY_DIR} -``` - -Deploy the content of ${WEB_DEPLOY_DIR} to your web server. - -## 4. Assemble the debug app (optional) ## - -Debugging the player in a cast device is a little bit cumbersome compared to -debugging in a desktop browser. For this reason there is a debug app which -contains the player parts which are not depending on the cast library in a -traditional HTML app which can be run in a desktop browser. - -``` -# WEB_DEPLOY_DIR=www -# mkdir ${WEB_DEPLOY_DIR} -# cp bazel-bin/debug_app.js ${WEB_DEPLOY_DIR} -# cp bazel-bin/debug_styles_bin.css ${WEB_DEPLOY_DIR} -# cp html/player.html ${WEB_DEPLOY_DIR} -``` - -Deploy the content of ${WEB_DEPLOY_DIR} to your web server. - -# Unit test - -Unit tests can be run by the command -``` -# bazel test ... -``` diff --git a/cast_receiver_app/WORKSPACE b/cast_receiver_app/WORKSPACE deleted file mode 100644 index e6be3b9026..0000000000 --- a/cast_receiver_app/WORKSPACE +++ /dev/null @@ -1,38 +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. - -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -http_archive( - name = "com_google_protobuf", - sha256 = "73fdad358857e120fd0fa19e071a96e15c0f23bb25f85d3f7009abfd4f264a2a", - strip_prefix = "protobuf-3.6.1.3", - urls = ["https://github.com/google/protobuf/archive/v3.6.1.3.tar.gz"], -) - -http_archive( - name = "io_bazel_rules_closure", - sha256 = "b29a8bc2cb10513c864cb1084d6f38613ef14a143797cea0af0f91cd385f5e8c", - strip_prefix = "rules_closure-0.8.0", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_closure/archive/0.8.0.tar.gz", - "https://github.com/bazelbuild/rules_closure/archive/0.8.0.tar.gz", - ], -) -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories") - -closure_repositories( - omit_com_google_protobuf = True, -) - diff --git a/cast_receiver_app/app-desktop/html/index.css b/cast_receiver_app/app-desktop/html/index.css deleted file mode 100644 index ff77e1cbfa..0000000000 --- a/cast_receiver_app/app-desktop/html/index.css +++ /dev/null @@ -1,156 +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. - */ -html, body, section, video, div, span, ul, li { - border: 0; - box-sizing: border-box; - margin: 0; - padding: 0; -} -body, html { - height: 100%; - overflow: auto; - background-color: #333; - color: #eeeeee; - font-family: Roboto, Arial, sans-serif; -} -body { - padding-top: 24px; -} -.exo_controls { - list-style: none; - padding: 0; - white-space: nowrap; - margin-top: 12px; -} -.exo_controls > li { - display: inline-block; - width: 72px; -} -.exo_controls > .large { - width: 140px; -} -/* an action element to add or remove a media item */ -.action { - margin: 4px auto; - max-width: 640px; -} -.action.prepared { - background-color: #AA0000; -} -/** marks whether a given media item is in the queue */ -.queue-marker { - background-color: #AA0000; - border-radius: 50%; - border: 1px solid #ffc0c0; - display: none; - float: right; - height: 1em; - margin-top: 1px; - width: 1em; -} -.action[data-uuid] .queue-marker { - display: inline-block; -} -.action.prepared .queue-marker { - background-color: #fff900; -} -.playing .action.prepared .queue-marker { - animation-name: spin; - animation-iteration-count: infinite; - animation-duration: 1.6s; -} -/* A simple button. */ -.button { - background-color: #45484d; - border: 1px solid #495267; - border-radius: 3px; - color: #FFFFFF; - cursor: pointer; - font-size: 12px; - font-weight: bold; - padding: 10px 10px 10px 10px; - text-decoration: none; - text-shadow: -1px -1px 0 rgba(0,0,0,0.3); - -webkit-user-select: none; -} -.button:hover { - border: 1px solid #363d4c; - background-color: #2d2f32; - background-image: linear-gradient(to bottom, #2d2f32, #1a1a1a); -} -.ribbon { - background-color: #003a5dc2; - box-shadow: 2px 2px 4px #000; - left: -60px; - height: 3.3em; - padding-top: 7px; - position: absolute; - text-align: center; - top: 27px; - transform: rotateZ(-45deg); - width: 220px; - border: 1px dashed #cacaca; - outline-color: #003a5dc2; - outline-width: 2px; - outline-style: solid; -} -.ribbon a { - color: white; - text-decoration: none; - -webkit-user-select: none; -} -#button_prepare { - left: 0; - position: absolute; -} -#button_stop { - position: absolute; - right: 0; -} -#exo_demo_view { - height: 360px; - margin: auto; - overflow: hidden; - position: relative; - width: 640px; -} -#video { - background-color: #000; - border-radius: 8px; - height: 100%; - margin-bottom: auto; - margin-top: auto; - width: 100%; -} -#exo_controls { - display: none; - margin: auto; - position: relative; - text-align: center; - width: 640px; -} -#media-actions { - margin-top: 12px; -} - -@keyframes spin { - from { - transform: rotateX(0deg); - } - to { - transform: rotateX(180deg); - } -} diff --git a/cast_receiver_app/app-desktop/html/index.html b/cast_receiver_app/app-desktop/html/index.html deleted file mode 100644 index 19a118913b..0000000000 --- a/cast_receiver_app/app-desktop/html/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - -

        -
        - -
        -
        -
        -
        -
        - - -
        -
        -
        - for debugging
        purpose only -
        -
        -
        -
          -
        • prepare
        • -
        • prev
        • -
        • rewind
        • -
        • play
        • -
        • pause
        • -
        • ffwd
        • -
        • next
        • -
        • stop
        • -
        -
        -
        -
        - - - diff --git a/cast_receiver_app/app-desktop/src/main.js b/cast_receiver_app/app-desktop/src/main.js deleted file mode 100644 index 5645d70787..0000000000 --- a/cast_receiver_app/app-desktop/src/main.js +++ /dev/null @@ -1,170 +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. - */ - -goog.module('exoplayer.cast.debug'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); -const Player = goog.require('exoplayer.cast.Player'); -const PlayerControls = goog.require('exoplayer.cast.PlayerControls'); -const ShakaPlayer = goog.require('shaka.Player'); -const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); -const installAll = goog.require('shaka.polyfill.installAll'); -const util = goog.require('exoplayer.cast.util'); - -/** @type {!Array} */ -let queue = []; -/** @type {number} */ -let uuidCounter = 1; - -// install all polyfills for the Shaka player -installAll(); - -/** - * Listens for player state changes and logs the state to the console. - * - * @param {!PlayerState} playerState The player state. - */ -const playerListener = function(playerState) { - util.log(['playerState: ', playerState.playbackPosition, playerState]); - queue = playerState.mediaQueue; - highlightCurrentItem( - playerState.playbackPosition && playerState.playbackPosition.uuid ? - playerState.playbackPosition.uuid : - ''); - if (playerState.playWhenReady && playerState.playbackState === 'READY') { - document.body.classList.add('playing'); - } else { - document.body.classList.remove('playing'); - } - if (playerState.playbackState === 'IDLE' && queue.length === 0) { - // Stop has been called or player not yet prepared. - resetSampleList(); - } -}; - -/** - * Highlights the currently playing item in the samples list. - * - * @param {string} uuid - */ -const highlightCurrentItem = function(uuid) { - const actions = /** @type {!NodeList} */ ( - document.querySelectorAll('#media-actions .action')); - for (let action of actions) { - if (action.dataset['uuid'] === uuid) { - action.classList.add('prepared'); - } else { - action.classList.remove('prepared'); - } - } -}; - -/** - * Makes sure all items reflect being removed from the timeline. - */ -const resetSampleList = function() { - const actions = /** @type {!NodeList} */ ( - document.querySelectorAll('#media-actions .action')); - for (let action of actions) { - action.classList.remove('prepared'); - delete action.dataset['uuid']; - } -}; - -/** - * If the arguments provide a valid media item it is added to the player. - * - * @param {!MediaItem} item The media item. - * @return {string} The uuid which has been created for the item before adding. - */ -const addQueueItem = function(item) { - if (!(item.media && item.media.uri && item.mimeType)) { - throw Error('insufficient arguments to add a queue item'); - } - item.uuid = 'uuid-' + uuidCounter++; - player.addQueueItems(queue.length, [item], /* playbackOrder= */ undefined); - return item.uuid; -}; - -/** - * An event listener which listens for actions. - * - * @param {!Event} ev The DOM event. - */ -const handleAction = (ev) => { - let target = ev.target; - while (target !== document.body && !target.dataset['action']) { - target = target.parentNode; - } - if (!target || !target.dataset['action']) { - return; - } - switch (target.dataset['action']) { - case 'player.addItems': - if (target.dataset['uuid']) { - player.removeQueueItems([target.dataset['uuid']]); - delete target.dataset['uuid']; - } else { - const uuid = addQueueItem(/** @type {!MediaItem} */ - (JSON.parse(target.dataset['item']))); - target.dataset['uuid'] = uuid; - } - break; - } -}; - -/** - * Appends samples to the list of media item actions. - * - * @param {!Array} mediaItems The samples to add. - */ -const appendSamples = function(mediaItems) { - const samplesList = document.getElementById('media-actions'); - mediaItems.forEach((item) => { - const div = /** @type {!HTMLElement} */ (document.createElement('div')); - div.classList.add('action', 'button'); - div.dataset['action'] = 'player.addItems'; - div.dataset['item'] = JSON.stringify(item); - div.appendChild(document.createTextNode(item.title)); - const marker = document.createElement('span'); - marker.classList.add('queue-marker'); - div.appendChild(marker); - samplesList.appendChild(div); - }); -}; - -/** @type {!HTMLMediaElement} */ -const mediaElement = - /** @type {!HTMLMediaElement} */ (document.getElementById('video')); -// Workaround for https://github.com/google/shaka-player/issues/1819 -// TODO(bachinger) Remove line when better fix available. -new SimpleTextDisplayer(mediaElement); -/** @type {!ShakaPlayer} */ -const shakaPlayer = new ShakaPlayer(mediaElement); -/** @type {!Player} */ -const player = new Player(shakaPlayer, new ConfigurationFactory()); -new PlayerControls(player, 'exo_controls'); -new PlaybackInfoView(player, 'exo_playback_info'); - -// register listeners -document.body.addEventListener('click', handleAction); -player.addPlayerListener(playerListener); - -// expose the player for debugging purposes. -window['player'] = player; - -exports.appendSamples = appendSamples; diff --git a/cast_receiver_app/app-desktop/src/player_controls.js b/cast_receiver_app/app-desktop/src/player_controls.js deleted file mode 100644 index e29f74148c..0000000000 --- a/cast_receiver_app/app-desktop/src/player_controls.js +++ /dev/null @@ -1,164 +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. - */ -goog.module('exoplayer.cast.PlayerControls'); - -const Player = goog.require('exoplayer.cast.Player'); - -/** - * A simple UI to control the player. - * - */ -class PlayerControls { - /** - * @param {!Player} player The player. - * @param {string} containerId The id of the container element. - */ - constructor(player, containerId) { - /** @const @private {!Player} */ - this.player_ = player; - /** @const @private {?Element} */ - this.root_ = document.getElementById(containerId); - /** @const @private {?Element} */ - this.playButton_ = this.root_.querySelector('#button_play'); - /** @const @private {?Element} */ - this.pauseButton_ = this.root_.querySelector('#button_pause'); - /** @const @private {?Element} */ - this.previousButton_ = this.root_.querySelector('#button_previous'); - /** @const @private {?Element} */ - this.nextButton_ = this.root_.querySelector('#button_next'); - - const previous = () => { - const index = player.getPreviousWindowIndex(); - if (index !== -1) { - player.seekToWindow(index, 0); - } - }; - const next = () => { - const index = player.getNextWindowIndex(); - if (index !== -1) { - player.seekToWindow(index, 0); - } - }; - const rewind = () => { - player.seekToWindow( - player.getCurrentWindowIndex(), - player.getCurrentPositionMs() - 15000); - }; - const fastForward = () => { - player.seekToWindow( - player.getCurrentWindowIndex(), - player.getCurrentPositionMs() + 30000); - }; - const actions = { - 'pwr_1': (ev) => player.setPlayWhenReady(true), - 'pwr_0': (ev) => player.setPlayWhenReady(false), - 'rewind': rewind, - 'fastforward': fastForward, - 'previous': previous, - 'next': next, - 'prepare': (ev) => player.prepare(), - 'stop': (ev) => player.stop(true), - 'remove_queue_item': (ev) => { - player.removeQueueItems([ev.target.dataset.id]); - }, - }; - /** - * @param {!Event} ev The key event. - * @return {boolean} true if the key event has been handled. - */ - const keyListener = (ev) => { - const key = /** @type {!KeyboardEvent} */ (ev).key; - switch (key) { - case 'ArrowUp': - case 'k': - previous(); - ev.preventDefault(); - return true; - case 'ArrowDown': - case 'j': - next(); - ev.preventDefault(); - return true; - case 'ArrowLeft': - case 'h': - rewind(); - ev.preventDefault(); - return true; - case 'ArrowRight': - case 'l': - fastForward(); - ev.preventDefault(); - return true; - case ' ': - case 'p': - player.setPlayWhenReady(!player.getPlayWhenReady()); - ev.preventDefault(); - return true; - } - return false; - }; - document.addEventListener('keydown', keyListener); - this.root_.addEventListener('click', function(ev) { - const method = ev.target['dataset']['method']; - if (actions[method]) { - actions[method](ev); - } - return true; - }); - player.addPlayerListener((playerState) => this.updateUi(playerState)); - player.invalidate(); - this.setVisible_(true); - } - - /** - * Syncs the ui with the player state. - * - * @param {!PlayerState} playerState The state of the player to be reflected - * by the UI. - */ - updateUi(playerState) { - if (playerState.playWhenReady) { - this.playButton_.style.display = 'none'; - this.pauseButton_.style.display = 'inline-block'; - } else { - this.playButton_.style.display = 'inline-block'; - this.pauseButton_.style.display = 'none'; - } - if (this.player_.getNextWindowIndex() === -1) { - this.nextButton_.style.visibility = 'hidden'; - } else { - this.nextButton_.style.visibility = 'visible'; - } - if (this.player_.getPreviousWindowIndex() === -1) { - this.previousButton_.style.visibility = 'hidden'; - } else { - this.previousButton_.style.visibility = 'visible'; - } - } - - /** - * @private - * @param {boolean} visible If `true` thie controls are shown. If `false` the - * controls are hidden. - */ - setVisible_(visible) { - if (this.root_) { - this.root_.style.display = visible ? 'block' : 'none'; - } - } -} - -exports = PlayerControls; diff --git a/cast_receiver_app/app-desktop/src/samples.js b/cast_receiver_app/app-desktop/src/samples.js deleted file mode 100644 index 2d190bdef4..0000000000 --- a/cast_receiver_app/app-desktop/src/samples.js +++ /dev/null @@ -1,70 +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. - */ -goog.module('exoplayer.cast.samples'); - -const {appendSamples} = goog.require('exoplayer.cast.debug'); - -appendSamples([ - { - title: 'DASH: multi-period', - mimeType: 'application/dash+xml', - media: { - uri: 'https://storage.googleapis.com/exoplayer-test-media-internal-6383' + - '4241aced7884c2544af1a3452e01/dash/multi-period/two-periods-minimal' + - '-duration.mpd', - }, - }, - { - title: 'HLS: Angel one', - mimeType: 'application/vnd.apple.mpegurl', - media: { - uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hl' + - 's.m3u8', - }, - }, - { - title: 'MP4: Elephants dream', - mimeType: 'video/*', - media: { - uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/' + - 'ElephantsDream.mp4', - }, - }, - { - title: 'MKV: Android screens', - mimeType: 'video/*', - media: { - uri: 'https://storage.googleapis.com/exoplayer-test-media-1/mkv/android' + - '-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv', - }, - }, - { - title: 'WV: HDCP not specified', - mimeType: 'application/dash+xml', - media: { - uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd', - }, - drmSchemes: [ - { - licenseServer: { - uri: 'https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1' + - 'c&provider=widevine_test', - }, - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - }, - ], - }, -]); diff --git a/cast_receiver_app/app-desktop/src/samples_internal.js b/cast_receiver_app/app-desktop/src/samples_internal.js deleted file mode 100644 index 71b05eb2c1..0000000000 --- a/cast_receiver_app/app-desktop/src/samples_internal.js +++ /dev/null @@ -1,79 +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. - */ -goog.module('exoplayer.cast.samplesinternal'); - -const {appendSamples} = goog.require('exoplayer.cast.debug'); - -appendSamples([ - { - title: 'DAS: VOD', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dash-pvr.zahs.tv/hd/manifest.mpd', - }, - }, - { - title: 'MP3', - mimeType: 'audio/*', - media: { - uri: 'http://www.noiseaddicts.com/samples_1w72b820/4190.mp3', - }, - }, - { - title: 'DASH: live', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dash-live.zahs.tv/sd/manifest.mpd', - }, - }, - { - title: 'HLS: live', - mimeType: 'application/vnd.apple.mpegurl', - media: { - uri: 'https://demo-hls5-live.zahs.tv/sd/master.m3u8', - }, - }, - { - title: 'Live DASH (HD/Widevine)', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine.mpd', - }, - drmSchemes: [ - { - licenseServer: { - uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', - }, - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - }, - ], - }, - { - title: 'VOD DASH (HD/Widevine)', - mimeType: 'application/dash+xml', - media: { - uri: 'https://demo-dashenc-pvr.zahs.tv/hd/widevine.mpd', - }, - drmSchemes: [ - { - licenseServer: { - uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license', - }, - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - }, - ], - }, -]); diff --git a/cast_receiver_app/app/html/index.css b/cast_receiver_app/app/html/index.css deleted file mode 100644 index dfc9b4e0e5..0000000000 --- a/cast_receiver_app/app/html/index.css +++ /dev/null @@ -1,39 +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. - */ -section, video, div, span, body, html { - border: 0; - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html, body { - background-color: #000; - height: 100%; - overflow: hidden; -} - -#exo_player_view { - background-color: #000; - height: 100%; - position: relative; -} - -#exo_video { - height: 100%; - width: 100%; -} - diff --git a/cast_receiver_app/app/html/index.html b/cast_receiver_app/app/html/index.html deleted file mode 100644 index 64de3e8a8e..0000000000 --- a/cast_receiver_app/app/html/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - -
        - -
        -
        -
        -
        -
        - - -
        -
        -
        - - - diff --git a/cast_receiver_app/app/html/playback_info_view.css b/cast_receiver_app/app/html/playback_info_view.css deleted file mode 100644 index f70695d873..0000000000 --- a/cast_receiver_app/app/html/playback_info_view.css +++ /dev/null @@ -1,59 +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. - */ - -.exo_text_label { - color: #fff; - font-family: Roboto, Arial, sans-serif; - font-size: 1em; - margin-top: 4px; -} - -#exo_playback_info { - bottom: 5%; - display: none; - left: 4%; - position: absolute; - right: 4%; - width: 92%; -} - -#exo_time_bar { - width: 100%; -} - -#exo_duration { - background-color: rgba(255, 255, 255, 0.4); - height: 0.5em; - overflow: hidden; - position: relative; - width: 100%; -} - -#exo_elapsed_time { - background-color: rgb(73, 128, 218); - height: 100%; - opacity: 1; - width: 0; -} - -#exo_duration_label { - float: right; -} - -#exo_elapsed_time_label { - float: left; -} - diff --git a/cast_receiver_app/app/src/main.js b/cast_receiver_app/app/src/main.js deleted file mode 100644 index 37c6fd41eb..0000000000 --- a/cast_receiver_app/app/src/main.js +++ /dev/null @@ -1,55 +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. - */ - -goog.module('exoplayer.cast.app'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); -const Player = goog.require('exoplayer.cast.Player'); -const Receiver = goog.require('exoplayer.cast.Receiver'); -const ShakaPlayer = goog.require('shaka.Player'); -const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer'); -const installAll = goog.require('shaka.polyfill.installAll'); - -/** - * The ExoPlayer namespace for messages sent and received via cast message bus. - */ -const MESSAGE_NAMESPACE_EXOPLAYER = 'urn:x-cast:com.google.exoplayer.cast'; - -// installs all polyfills for the Shaka player -installAll(); -/** @type {?HTMLMediaElement} */ -const videoElement = - /** @type {?HTMLMediaElement} */ (document.getElementById('exo_video')); -if (videoElement !== null) { - // Workaround for https://github.com/google/shaka-player/issues/1819 - // TODO(bachinger) Remove line when better fix available. - new SimpleTextDisplayer(videoElement); - /** @type {!cast.framework.CastReceiverContext} */ - const castReceiverContext = cast.framework.CastReceiverContext.getInstance(); - const shakaPlayer = new ShakaPlayer(/** @type {!HTMLMediaElement} */ - (videoElement)); - const player = new Player(shakaPlayer, new ConfigurationFactory()); - new PlaybackInfoView(player, 'exo_playback_info'); - if (castReceiverContext !== null) { - const messageDispatcher = - new MessageDispatcher(MESSAGE_NAMESPACE_EXOPLAYER, castReceiverContext); - new Receiver(player, castReceiverContext, messageDispatcher); - } - // expose player for debugging purposes. - window['player'] = player; -} diff --git a/cast_receiver_app/app/src/message_dispatcher.js b/cast_receiver_app/app/src/message_dispatcher.js deleted file mode 100644 index 151ac87fbe..0000000000 --- a/cast_receiver_app/app/src/message_dispatcher.js +++ /dev/null @@ -1,234 +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. - */ -goog.module('exoplayer.cast.MessageDispatcher'); - -const validation = goog.require('exoplayer.cast.validation'); - -/** - * A callback function which is called by an action handler to indicate when - * processing has completed. - * - * @typedef {function(?PlayerState): undefined} - */ -const Callback = undefined; - -/** - * Handles an action sent by a sender app. - * - * @typedef {function(!Object, number, string, !Callback): undefined} - */ -const ActionHandler = undefined; - -/** - * Dispatches messages of a cast message bus to registered action handlers. - * - *

        The dispatcher listens to events of a CastMessageBus for the namespace - * passed to the constructor. The data property of the event is - * parsed as a json document and delegated to a handler registered for the given - * method. - */ -class MessageDispatcher { - /** - * @param {string} namespace The message namespace. - * @param {!cast.framework.CastReceiverContext} castReceiverContext The cast - * receiver manager. - */ - constructor(namespace, castReceiverContext) { - /** @private @const {string} */ - this.namespace_ = namespace; - /** @private @const {!cast.framework.CastReceiverContext} */ - this.castReceiverContext_ = castReceiverContext; - /** @private @const {!Array} */ - this.messageQueue_ = []; - /** @private @const {!Object} */ - this.actions_ = {}; - /** @private @const {!Object} */ - this.senderSequences_ = {}; - /** @private @const {function(string, *)} */ - this.jsonStringifyReplacer_ = (key, value) => { - if (value === Infinity || value === null) { - return undefined; - } - return value; - }; - this.castReceiverContext_.addCustomMessageListener( - this.namespace_, this.onMessage.bind(this)); - } - - /** - * Registers a handler of a given action. - * - * @param {string} method The method name for which to register the handler. - * @param {!Array>} argDefs The name and type of each argument - * or an empty array if the method has no arguments. - * @param {!ActionHandler} handler A function to process the action. - */ - registerActionHandler(method, argDefs, handler) { - this.actions_[method] = { - method, - argDefs, - handler, - }; - } - - /** - * Unregisters the handler of the given action. - * - * @param {string} action The action to unregister. - */ - unregisterActionHandler(action) { - delete this.actions_[action]; - } - - /** - * Callback to receive messages sent by sender apps. - * - * @param {!cast.framework.system.Event} event The event received from the - * sender app. - */ - onMessage(event) { - console.log('message arrived from sender', this.namespace_, event); - const message = /** @type {!ExoCastMessage} */ (event.data); - const action = this.actions_[message.method]; - if (action) { - const args = message.args; - for (let i = 0; i < action.argDefs.length; i++) { - if (!validation.validateProperty( - args, action.argDefs[i][0], action.argDefs[i][1])) { - console.warn('invalid method call', message); - return; - } - } - this.messageQueue_.push({ - senderId: event.senderId, - message: message, - handler: action.handler - }); - if (this.messageQueue_.length === 1) { - this.executeNext(); - } else { - // Do nothing. An action is executing asynchronously and will call - // executeNext when finished. - } - } else { - console.warn('handler of method not found', message); - } - } - - /** - * Executes the next message in the queue. - */ - executeNext() { - if (this.messageQueue_.length === 0) { - return; - } - const head = this.messageQueue_[0]; - const message = head.message; - const senderSequence = message.sequenceNumber; - this.senderSequences_[head.senderId] = senderSequence; - try { - head.handler(message.args, senderSequence, head.senderId, (response) => { - if (response) { - this.send(head.senderId, response); - } - this.shiftPendingMessage_(head); - }); - } catch (e) { - this.shiftPendingMessage_(head); - console.error('error while executing method : ' + message.method, e); - } - } - - /** - * Broadcasts the sender state to all sender apps registered for the - * given message namespace. - * - * @param {!PlayerState} playerState The player state to be sent. - */ - broadcast(playerState) { - this.castReceiverContext_.getSenders().forEach((sender) => { - this.send(sender.id, playerState); - }); - delete playerState.sequenceNumber; - } - - /** - * Sends the PlayerState to the given sender. - * - * @param {string} senderId The id of the sender. - * @param {!PlayerState} playerState The message to send. - */ - send(senderId, playerState) { - playerState.sequenceNumber = this.senderSequences_[senderId] || -1; - this.castReceiverContext_.sendCustomMessage( - this.namespace_, senderId, - // TODO(bachinger) Find a better solution. - JSON.parse(JSON.stringify(playerState, this.jsonStringifyReplacer_))); - } - - /** - * Notifies the message dispatcher that a given sender has disconnected from - * the receiver. - * - * @param {string} senderId The id of the sender. - */ - notifySenderDisconnected(senderId) { - delete this.senderSequences_[senderId]; - } - - /** - * Shifts the pending message and executes the next if any. - * - * @private - * @param {!Message} pendingMessage The pending message. - */ - shiftPendingMessage_(pendingMessage) { - if (pendingMessage === this.messageQueue_[0]) { - this.messageQueue_.shift(); - this.executeNext(); - } - } -} - -/** - * An item in the message queue. - * - * @record - */ -function Message() {} - -/** - * The sender id. - * - * @type {string} - */ -Message.prototype.senderId; - -/** - * The ExoCastMessage sent by the sender app. - * - * @type {!ExoCastMessage} - */ -Message.prototype.message; - -/** - * The handler function handling the message. - * - * @type {!ActionHandler} - */ -Message.prototype.handler; - -exports = MessageDispatcher; diff --git a/cast_receiver_app/app/src/receiver.js b/cast_receiver_app/app/src/receiver.js deleted file mode 100644 index 5e67219e75..0000000000 --- a/cast_receiver_app/app/src/receiver.js +++ /dev/null @@ -1,191 +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. - */ - -goog.module('exoplayer.cast.Receiver'); - -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const Player = goog.require('exoplayer.cast.Player'); -const validation = goog.require('exoplayer.cast.validation'); - -/** - * The Receiver receives messages from a message bus and delegates to - * the player. - * - * @constructor - * @param {!Player} player The player. - * @param {!cast.framework.CastReceiverContext} context The cast receiver - * context. - * @param {!MessageDispatcher} messageDispatcher The message dispatcher to use. - */ -const Receiver = function(player, context, messageDispatcher) { - addPlayerActions(messageDispatcher, player); - addQueueActions(messageDispatcher, player); - player.addPlayerListener((playerState) => { - messageDispatcher.broadcast(playerState); - }); - - context.addEventListener( - cast.framework.system.EventType.SENDER_CONNECTED, (event) => { - messageDispatcher.send(event.senderId, player.getPlayerState()); - }); - - context.addEventListener( - cast.framework.system.EventType.SENDER_DISCONNECTED, (event) => { - messageDispatcher.notifySenderDisconnected(event.senderId); - if (event.reason === - cast.framework.system.DisconnectReason.REQUESTED_BY_SENDER && - context.getSenders().length === 0) { - window.close(); - } - }); - - // Start the cast receiver context. - context.start(); -}; - -/** - * Registers action handlers for playback messages sent by the sender app. - * - * @param {!MessageDispatcher} messageDispatcher The dispatcher. - * @param {!Player} player The player. - */ -const addPlayerActions = function(messageDispatcher, player) { - messageDispatcher.registerActionHandler( - 'player.setPlayWhenReady', [['playWhenReady', 'boolean']], - (args, senderSequence, senderId, callback) => { - const playWhenReady = args['playWhenReady']; - callback( - !player.setPlayWhenReady(playWhenReady) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.seekTo', - [ - ['uuid', 'string'], - ['positionMs', '?number'], - ], - (args, senderSequence, senderId, callback) => { - callback( - !player.seekToUuid(args['uuid'], args['positionMs']) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.setRepeatMode', [['repeatMode', 'RepeatMode']], - (args, senderSequence, senderId, callback) => { - callback( - !player.setRepeatMode(args['repeatMode']) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.setShuffleModeEnabled', [['shuffleModeEnabled', 'boolean']], - (args, senderSequence, senderId, callback) => { - callback( - !player.setShuffleModeEnabled(args['shuffleModeEnabled']) ? - player.getPlayerState() : - null); - }); - messageDispatcher.registerActionHandler( - 'player.onClientConnected', [], - (args, senderSequence, senderId, callback) => { - callback(player.getPlayerState()); - }); - messageDispatcher.registerActionHandler( - 'player.stop', [['reset', 'boolean']], - (args, senderSequence, senderId, callback) => { - player.stop(args['reset']).then(() => { - callback(null); - }); - }); - messageDispatcher.registerActionHandler( - 'player.prepare', [], (args, senderSequence, senderId, callback) => { - player.prepare(); - callback(null); - }); - messageDispatcher.registerActionHandler( - 'player.setTrackSelectionParameters', - [ - ['preferredAudioLanguage', 'string'], - ['preferredTextLanguage', 'string'], - ['disabledTextTrackSelectionFlags', 'Array'], - ['selectUndeterminedTextLanguage', 'boolean'], - ], - (args, senderSequence, senderId, callback) => { - const trackSelectionParameters = - /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: args['preferredAudioLanguage'], - preferredTextLanguage: args['preferredTextLanguage'], - disabledTextTrackSelectionFlags: - args['disabledTextTrackSelectionFlags'], - selectUndeterminedTextLanguage: - args['selectUndeterminedTextLanguage'], - }); - callback( - !player.setTrackSelectionParameters(trackSelectionParameters) ? - player.getPlayerState() : - null); - }); -}; - -/** - * Registers action handlers for queue management messages sent by the sender - * app. - * - * @param {!MessageDispatcher} messageDispatcher The dispatcher. - * @param {!Player} player The player. - */ -const addQueueActions = - function (messageDispatcher, player) { - messageDispatcher.registerActionHandler( - 'player.addItems', - [ - ['index', '?number'], - ['items', 'Array'], - ['shuffleOrder', 'Array'], - ], - (args, senderSequence, senderId, callback) => { - const mediaItems = args['items']; - const index = args['index'] || player.getQueueSize(); - let addedItemCount; - if (validation.validateMediaItems(mediaItems)) { - addedItemCount = - player.addQueueItems(index, mediaItems, args['shuffleOrder']); - } - callback(addedItemCount === 0 ? player.getPlayerState() : null); - }); - messageDispatcher.registerActionHandler( - 'player.removeItems', [['uuids', 'Array']], - (args, senderSequence, senderId, callback) => { - const removedItemsCount = player.removeQueueItems(args['uuids']); - callback(removedItemsCount === 0 ? player.getPlayerState() : null); - }); - messageDispatcher.registerActionHandler( - 'player.moveItem', - [ - ['uuid', 'string'], - ['index', 'number'], - ['shuffleOrder', 'Array'], - ], - (args, senderSequence, senderId, callback) => { - const hasMoved = player.moveQueueItem( - args['uuid'], args['index'], args['shuffleOrder']); - callback(!hasMoved ? player.getPlayerState() : null); - }); -}; - -exports = Receiver; diff --git a/cast_receiver_app/app/src/validation.js b/cast_receiver_app/app/src/validation.js deleted file mode 100644 index 23e2708f8e..0000000000 --- a/cast_receiver_app/app/src/validation.js +++ /dev/null @@ -1,163 +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. - * - * @fileoverview A validator for messages received from sender apps. - */ - -goog.module('exoplayer.cast.validation'); - -const {getPlaybackType, PlaybackType, RepeatMode} = goog.require('exoplayer.cast.constants'); - -/** - * Media item fields. - * - * @enum {string} - */ -const MediaItemField = { - UUID: 'uuid', - MEDIA: 'media', - MIME_TYPE: 'mimeType', - DRM_SCHEMES: 'drmSchemes', - TITLE: 'title', - DESCRIPTION: 'description', - START_POSITION_US: 'startPositionUs', - END_POSITION_US: 'endPositionUs', -}; - -/** - * DrmScheme fields. - * - * @enum {string} - */ -const DrmSchemeField = { - UUID: 'uuid', - LICENSE_SERVER_URI: 'licenseServer', -}; - -/** - * UriBundle fields. - * - * @enum {string} - */ -const UriBundleField = { - URI: 'uri', - REQUEST_HEADERS: 'requestHeaders', -}; - -/** - * Validates an array of media items. - * - * @param {!Array} mediaItems An array of media items. - * @return {boolean} true if all media items are valid, otherwise false is - * returned. - */ -const validateMediaItems = function (mediaItems) { - for (let i = 0; i < mediaItems.length; i++) { - if (!validateMediaItem(mediaItems[i])) { - return false; - } - } - return true; -}; - -/** - * Validates a queue item sent to the receiver by a sender app. - * - * @param {!MediaItem} mediaItem The media item. - * @return {boolean} true if the media item is valid, false otherwise. - */ -const validateMediaItem = function (mediaItem) { - // validate minimal properties - if (!validateProperty(mediaItem, MediaItemField.UUID, 'string')) { - console.log('missing mandatory uuid', mediaItem.uuid); - return false; - } - if (!validateProperty(mediaItem.media, UriBundleField.URI, 'string')) { - console.log('missing mandatory', mediaItem.media ? 'uri' : 'media'); - return false; - } - const mimeType = mediaItem.mimeType; - if (!mimeType || getPlaybackType(mimeType) === PlaybackType.UNKNOWN) { - console.log('unsupported mime type:', mimeType); - return false; - } - // validate optional properties - if (goog.isArray(mediaItem.drmSchemes)) { - for (let i = 0; i < mediaItem.drmSchemes.length; i++) { - let drmScheme = mediaItem.drmSchemes[i]; - if (!validateProperty(drmScheme, DrmSchemeField.UUID, 'string') || - !validateProperty( - drmScheme.licenseServer, UriBundleField.URI, 'string')) { - console.log('invalid drm scheme', drmScheme); - return false; - } - } - } - if (!validateProperty(mediaItem, MediaItemField.START_POSITION_US, '?number') - || !validateProperty(mediaItem, MediaItemField.END_POSITION_US, '?number') - || !validateProperty(mediaItem, MediaItemField.TITLE, '?string') - || !validateProperty(mediaItem, MediaItemField.DESCRIPTION, '?string')) { - console.log('invalid type of one of startPositionUs, endPositionUs, title' - + ' or description', mediaItem); - return false; - } - return true; -}; - -/** - * Validates the existence and type of a property. - * - *

        Supported types: number, string, boolean, Array. - *

        Prefix the type with a ? to indicate that the property is optional. - * - * @param {?Object|?MediaItem|?UriBundle} obj The object to validate. - * @param {string} propertyName The name of the property. - * @param {string} type The type of the property. - * @return {boolean} True if valid, false otherwise. - */ -const validateProperty = function (obj, propertyName, type) { - if (typeof obj === 'undefined' || obj === null) { - return false; - } - const isOptional = type.startsWith('?'); - const value = obj[propertyName]; - if (isOptional && typeof value === 'undefined') { - return true; - } - type = isOptional ? type.substring(1) : type; - switch (type) { - case 'string': - return typeof value === 'string' || value instanceof String; - case 'number': - return typeof value === 'number' && isFinite(value); - case 'Array': - return typeof value !== 'undefined' && typeof value === 'object' - && value.constructor === Array; - case 'boolean': - return typeof value === 'boolean'; - case 'RepeatMode': - return value === RepeatMode.OFF || value === RepeatMode.ONE || - value === RepeatMode.ALL; - default: - console.warn('Unsupported type when validating an object property. ' + - 'Supported types are string, number, boolean and Array.', type); - return false; - } -}; - -exports.validateMediaItem = validateMediaItem; -exports.validateMediaItems = validateMediaItems; -exports.validateProperty = validateProperty; - diff --git a/cast_receiver_app/assemble.bazel.sh b/cast_receiver_app/assemble.bazel.sh deleted file mode 100755 index d2039a5152..0000000000 --- a/cast_receiver_app/assemble.bazel.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/bash -# 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. - -## -# Assembles the html, css and javascript files which have been created by the -# bazel build in a destination directory. - -HTML_DIR=app/html -HTML_DEBUG_DIR=app-desktop/html -BIN=bazel-bin - -function usage { - echo "usage: `basename "$0"` -d=DESTINATION_DIR" -} - -for i in "$@" -do -case $i in - -d=*|--destination=*) - DESTINATION="${i#*=}" - shift # past argument=value - ;; - -h|--help) - usage - exit 0 - ;; - *) - # unknown option - ;; -esac -done - -if [ ! -d "$DESTINATION" ]; then - echo "destination directory '$DESTINATION' is not declared or is not a\ - directory" - usage - exit 1 -fi - -if [ ! -f "$BIN/app.js" ];then - echo "file $BIN/app.js not found. Did you build already with bazel?" - echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" - exit 1 -fi - -if [ ! -f "$BIN/app_desktop.js" ];then - echo "file $BIN/app_desktop.js not found. Did you build already with bazel?" - echo "-> # bazel build .. --incompatible_package_name_is_a_function=false" - exit 1 -fi - -echo "assembling receiver and desktop app in $DESTINATION" -echo "-------" - -# cleaning up asset files in destination directory -FILES=( - app.js - app_desktop.js - app_styles.css - app_desktop_styles.css - index.html - player.html -) -for file in ${FILES[@]}; do - if [ -f $DESTINATION/$file ]; then - echo "deleting $file" - rm -f $DESTINATION/$file - fi -done -echo "-------" - -echo "copy html files to $DESTINATION" -cp $HTML_DIR/index.html $DESTINATION -cp $HTML_DEBUG_DIR/index.html $DESTINATION/player.html -echo "copy javascript files to $DESTINATION" -cp $BIN/app.js $BIN/app_desktop.js $DESTINATION -echo "copy css style to $DESTINATION" -cp $BIN/app_styles.css $BIN/app_desktop_styles.css $DESTINATION -echo "-------" - -echo "done." diff --git a/cast_receiver_app/externs/protocol.js b/cast_receiver_app/externs/protocol.js deleted file mode 100644 index d6544a6f37..0000000000 --- a/cast_receiver_app/externs/protocol.js +++ /dev/null @@ -1,489 +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. - */ - -/** - * @fileoverview Externs for messages sent by a sender app in JSON format. - * - * Fields defined here are prevented from being renamed by the js compiler. - * - * @externs - */ - -/** - * An uri bundle with an uri and request parameters. - * - * @record - */ -class UriBundle { - constructor() { - /** - * The URI. - * - * @type {string} - */ - this.uri; - - /** - * The request headers. - * - * @type {?Object} - */ - this.requestHeaders; - } -} - -/** - * @record - */ -class DrmScheme { - constructor() { - /** - * The DRM UUID. - * - * @type {string} - */ - this.uuid; - - /** - * The license URI. - * - * @type {?UriBundle} - */ - this.licenseServer; - } -} - -/** - * @record - */ -class MediaItem { - constructor() { - /** - * The uuid of the item. - * - * @type {string} - */ - this.uuid; - - /** - * The mime type. - * - * @type {string} - */ - this.mimeType; - - /** - * The media uri bundle. - * - * @type {!UriBundle} - */ - this.media; - - /** - * The DRM schemes. - * - * @type {!Array} - */ - this.drmSchemes; - - /** - * The position to start playback from. - * - * @type {number} - */ - this.startPositionUs; - - /** - * The position at which to end playback. - * - * @type {number} - */ - this.endPositionUs; - - /** - * The title of the media item. - * - * @type {string} - */ - this.title; - - /** - * The description of the media item. - * - * @type {string} - */ - this.description; - } -} - -/** - * Constraint parameters for track selection. - * - * @record - */ -class TrackSelectionParameters { - constructor() { - /** - * The preferred audio language. - * - * @type {string|undefined} - */ - this.preferredAudioLanguage; - - /** - * The preferred text language. - * - * @type {string|undefined} - */ - this.preferredTextLanguage; - - /** - * List of selection flags that are disabled for text track selections. - * - * @type {!Array} - */ - this.disabledTextTrackSelectionFlags; - - /** - * Whether a text track with undetermined language should be selected if no - * track with `preferredTextLanguage` is available, or if - * `preferredTextLanguage` is unset. - * - * @type {boolean} - */ - this.selectUndeterminedTextLanguage; - } -} - -/** - * The PlaybackPosition defined by the position, the uuid of the media item and - * the period id. - * - * @record - */ -class PlaybackPosition { - constructor() { - /** - * The current playback position in milliseconds. - * - * @type {number} - */ - this.positionMs; - - /** - * The uuid of the media item. - * - * @type {string} - */ - this.uuid; - - /** - * The id of the currently playing period. - * - * @type {string} - */ - this.periodId; - - /** - * The reason of a position discontinuity if any. - * - * @type {?string} - */ - this.discontinuityReason; - } -} - -/** - * The playback parameters. - * - * @record - */ -class PlaybackParameters { - constructor() { - /** - * The playback speed. - * - * @type {number} - */ - this.speed; - - /** - * The playback pitch. - * - * @type {number} - */ - this.pitch; - - /** - * Whether silence is skipped. - * - * @type {boolean} - */ - this.skipSilence; - } -} -/** - * The player state. - * - * @record - */ -class PlayerState { - constructor() { - /** - * The playback state. - * - * @type {string} - */ - this.playbackState; - - /** - * The playback parameters. - * - * @type {!PlaybackParameters} - */ - this.playbackParameters; - - /** - * Playback starts when ready if true. - * - * @type {boolean} - */ - this.playWhenReady; - - /** - * The current position within the media. - * - * @type {?PlaybackPosition} - */ - this.playbackPosition; - - /** - * The current window index. - * - * @type {number} - */ - this.windowIndex; - - /** - * The number of windows. - * - * @type {number} - */ - this.windowCount; - - /** - * The audio tracks. - * - * @type {!Array} - */ - this.audioTracks; - - /** - * The video tracks in case of adaptive media. - * - * @type {!Array>} - */ - this.videoTracks; - - /** - * The repeat mode. - * - * @type {string} - */ - this.repeatMode; - - /** - * Whether the shuffle mode is enabled. - * - * @type {boolean} - */ - this.shuffleModeEnabled; - - /** - * The playback order to use when shuffle mode is enabled. - * - * @type {!Array} - */ - this.shuffleOrder; - - /** - * The queue of media items. - * - * @type {!Array} - */ - this.mediaQueue; - - /** - * The media item info of the queue items if available. - * - * @type {!Object} - */ - this.mediaItemsInfo; - - /** - * The sequence number of the sender. - * - * @type {number} - */ - this.sequenceNumber; - - /** - * The player error. - * - * @type {?PlayerError} - */ - this.error; - } -} - -/** - * The error description. - * - * @record - */ -class PlayerError { - constructor() { - /** - * The error message. - * - * @type {string} - */ - this.message; - - /** - * The error code. - * - * @type {number} - */ - this.code; - - /** - * The error category. - * - * @type {number} - */ - this.category; - } -} - -/** - * A period. - * - * @record - */ -class Period { - constructor() { - /** - * The id of the period. Must be unique within a media item. - * - * @type {string} - */ - this.id; - - /** - * The duration of the period in microseconds. - * - * @type {number} - */ - this.durationUs; - } -} -/** - * Holds dynamic information for a MediaItem. - * - *

        Holds information related to preparation for a specific {@link MediaItem}. - * Unprepared items are associated with an {@link #EMPTY} info object until - * prepared. - * - * @record - */ -class MediaItemInfo { - constructor() { - /** - * The duration of the window in microseconds. - * - * @type {number} - */ - this.windowDurationUs; - - /** - * The default start position relative to the start of the window in - * microseconds. - * - * @type {number} - */ - this.defaultStartPositionUs; - - /** - * The periods conforming the media item. - * - * @type {!Array} - */ - this.periods; - - /** - * The position of the window in the first period in microseconds. - * - * @type {number} - */ - this.positionInFirstPeriodUs; - - /** - * Whether it is possible to seek within the window. - * - * @type {boolean} - */ - this.isSeekable; - - /** - * Whether the window may change when the timeline is updated. - * - * @type {boolean} - */ - this.isDynamic; - } -} - -/** - * The message envelope send by a sender app. - * - * @record - */ -class ExoCastMessage { - constructor() { - /** - * The clients message sequenec number. - * - * @type {number} - */ - this.sequenceNumber; - - /** - * The name of the method. - * - * @type {string} - */ - this.method; - - /** - * The arguments of the method. - * - * @type {!Object} - */ - this.args; - } -}; - diff --git a/cast_receiver_app/externs/shaka.js b/cast_receiver_app/externs/shaka.js deleted file mode 100644 index 0af36d7b8c..0000000000 --- a/cast_receiver_app/externs/shaka.js +++ /dev/null @@ -1,68 +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. - */ - -/** - * @fileoverview Externs of the Shaka configuration. - * - * @externs - */ - -/** - * The drm configuration for the Shaka player. - * - * @record - */ -class DrmConfiguration { - constructor() { - /** - * A map of license servers with the UUID of the drm system as the key and the - * license uri as the value. - * - * @type {!Object} - */ - this.servers; - } -} - -/** - * The configuration of the Shaka player. - * - * @record - */ -class PlayerConfiguration { - constructor() { - /** - * The preferred audio language. - * - * @type {string} - */ - this.preferredAudioLanguage; - - /** - * The preferred text language. - * - * @type {string} - */ - this.preferredTextLanguage; - - /** - * The drm configuration. - * - * @type {?DrmConfiguration} - */ - this.drm; - } -} diff --git a/cast_receiver_app/src/configuration_factory.js b/cast_receiver_app/src/configuration_factory.js deleted file mode 100644 index 819e52a755..0000000000 --- a/cast_receiver_app/src/configuration_factory.js +++ /dev/null @@ -1,90 +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. - */ - -goog.module('exoplayer.cast.ConfigurationFactory'); - -const {DRM_SYSTEMS} = goog.require('exoplayer.cast.constants'); - -const EMPTY_DRM_CONFIGURATION = - /** @type {!DrmConfiguration} */ (Object.freeze({ - servers: {}, - })); - -/** - * Creates the configuration of the Shaka player. - */ -class ConfigurationFactory { - /** - * Creates the Shaka player configuration. - * - * @param {!MediaItem} mediaItem The media item for which to create the - * configuration. - * @param {!TrackSelectionParameters} trackSelectionParameters The track - * selection parameters. - * @return {!PlayerConfiguration} The shaka player configuration. - */ - createConfiguration(mediaItem, trackSelectionParameters) { - const configuration = /** @type {!PlayerConfiguration} */ ({}); - this.mapLanguageConfiguration(trackSelectionParameters, configuration); - this.mapDrmConfiguration_(mediaItem, configuration); - return configuration; - } - - /** - * Maps the preferred audio and text language from the track selection - * parameters to the configuration. - * - * @param {!TrackSelectionParameters} trackSelectionParameters The selection - * parameters. - * @param {!PlayerConfiguration} playerConfiguration The player configuration. - */ - mapLanguageConfiguration(trackSelectionParameters, playerConfiguration) { - playerConfiguration.preferredAudioLanguage = - trackSelectionParameters.preferredAudioLanguage || ''; - playerConfiguration.preferredTextLanguage = - trackSelectionParameters.preferredTextLanguage || ''; - } - - /** - * Maps the drm configuration from the media item to the configuration. If no - * drm is specified for the given media item, null is assigned. - * - * @private - * @param {!MediaItem} mediaItem The media item. - * @param {!PlayerConfiguration} playerConfiguration The player configuration. - */ - mapDrmConfiguration_(mediaItem, playerConfiguration) { - if (!mediaItem.drmSchemes) { - playerConfiguration.drm = EMPTY_DRM_CONFIGURATION; - return; - } - const drmConfiguration = /** @type {!DrmConfiguration} */({ - servers: {}, - }); - let hasDrmServer = false; - mediaItem.drmSchemes.forEach((scheme) => { - const drmSystem = DRM_SYSTEMS[scheme.uuid]; - if (drmSystem && scheme.licenseServer && scheme.licenseServer.uri) { - hasDrmServer = true; - drmConfiguration.servers[drmSystem] = scheme.licenseServer.uri; - } - }); - playerConfiguration.drm = - hasDrmServer ? drmConfiguration : EMPTY_DRM_CONFIGURATION; - } -} - -exports = ConfigurationFactory; diff --git a/cast_receiver_app/src/constants.js b/cast_receiver_app/src/constants.js deleted file mode 100644 index e9600429f0..0000000000 --- a/cast_receiver_app/src/constants.js +++ /dev/null @@ -1,140 +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. - */ - -goog.module('exoplayer.cast.constants'); - -/** - * The underyling player. - * - * @enum {number} - */ -const PlaybackType = { - VIDEO_ELEMENT: 1, - SHAKA_PLAYER: 2, - UNKNOWN: 999, -}; - -/** - * Supported mime types and their playback mode. - * - * @type {!Object} - */ -const SUPPORTED_MIME_TYPES = Object.freeze({ - 'application/dash+xml': PlaybackType.SHAKA_PLAYER, - 'application/vnd.apple.mpegurl': PlaybackType.SHAKA_PLAYER, - 'application/vnd.ms-sstr+xml': PlaybackType.SHAKA_PLAYER, - 'application/x-mpegURL': PlaybackType.SHAKA_PLAYER, -}); - -/** - * Returns the playback type required for a given mime type, or - * PlaybackType.UNKNOWN if the mime type is not recognized. - * - * @param {string} mimeType The mime type. - * @return {!PlaybackType} The required playback type, or PlaybackType.UNKNOWN - * if the mime type is not recognized. - */ -const getPlaybackType = function(mimeType) { - if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) { - return PlaybackType.VIDEO_ELEMENT; - } else { - return SUPPORTED_MIME_TYPES[mimeType] || PlaybackType.UNKNOWN; - } -}; - -/** - * Error messages. - * - * @enum {string} - */ -const ErrorMessages = { - SHAKA_LOAD_ERROR: 'Error while loading media with Shaka.', - SHAKA_UNKNOWN_ERROR: 'Shaka error event captured.', - MEDIA_ELEMENT_UNKNOWN_ERROR: 'Media element error event captured.', - UNKNOWN_FATAL_ERROR: 'Fatal playback error. Shaka instance replaced.', - UNKNOWN_ERROR: 'Unknown error', -}; - -/** - * ExoPlayer's repeat modes. - * - * @enum {string} - */ -const RepeatMode = { - OFF: 'OFF', - ONE: 'ONE', - ALL: 'ALL', -}; - -/** - * Error categories. Error categories coming from Shaka are defined in [Shaka - * source - * code](https://shaka-player-demo.appspot.com/docs/api/shaka.util.Error.html). - * - * @enum {number} - */ -const ErrorCategory = { - MEDIA_ELEMENT: 0, - FATAL_SHAKA_ERROR: 1000, -}; - -/** - * An error object to be used if no media error is assigned to the `error` - * field of the media element when an error event is fired - * - * @type {!PlayerError} - */ -const UNKNOWN_ERROR = /** @type {!PlayerError} */ (Object.freeze({ - message: ErrorMessages.UNKNOWN_ERROR, - code: 0, - category: 0, -})); - -/** - * UUID for the Widevine DRM scheme. - * - * @type {string} - */ -const WIDEVINE_UUID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; - -/** - * UUID for the PlayReady DRM scheme. - * - * @type {string} - */ -const PLAYREADY_UUID = '9a04f079-9840-4286-ab92-e65be0885f95'; - -/** @type {!Object} */ -const drmSystems = {}; -drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha'; -drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready'; - -/** - * The uuids of the supported DRM systems. - * - * @type {!Object} - */ -const DRM_SYSTEMS = Object.freeze(drmSystems); - -exports.PlaybackType = PlaybackType; -exports.ErrorMessages = ErrorMessages; -exports.ErrorCategory = ErrorCategory; -exports.RepeatMode = RepeatMode; -exports.getPlaybackType = getPlaybackType; -exports.WIDEVINE_UUID = WIDEVINE_UUID; -exports.PLAYREADY_UUID = PLAYREADY_UUID; -exports.DRM_SYSTEMS = DRM_SYSTEMS; -exports.UNKNOWN_ERROR = UNKNOWN_ERROR; diff --git a/cast_receiver_app/src/playback_info_view.js b/cast_receiver_app/src/playback_info_view.js deleted file mode 100644 index 22e2b8ded5..0000000000 --- a/cast_receiver_app/src/playback_info_view.js +++ /dev/null @@ -1,233 +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. - */ - -goog.module('exoplayer.cast.PlaybackInfoView'); - -const Player = goog.require('exoplayer.cast.Player'); -const Timeout = goog.require('exoplayer.cast.Timeout'); -const dom = goog.require('goog.dom'); - -/** The default timeout for hiding the UI in milliseconds. */ -const SHOW_TIMEOUT_MS = 5000; -/** The timeout for hiding the UI in audio only mode in milliseconds. */ -const SHOW_TIMEOUT_MS_AUDIO = 0; -/** The timeout for updating the UI while being displayed. */ -const UPDATE_TIMEOUT_MS = 1000; - -/** - * Formats a duration in milliseconds to a string in hh:mm:ss format. - * - * @param {number} durationMs The duration in milliseconds. - * @return {string} The duration formatted as hh:mm:ss. - */ -const formatTimestampMsAsString = function (durationMs) { - const hours = Math.floor(durationMs / 1000 / 60 / 60); - const minutes = Math.floor((durationMs / 1000 / 60) % 60); - const seconds = Math.floor((durationMs / 1000) % 60) % 60; - let timeString = ''; - if (hours > 0) { - timeString += hours + ':'; - } - if (minutes < 10) { - timeString += '0'; - } - timeString += minutes + ":"; - if (seconds < 10) { - timeString += '0'; - } - timeString += seconds; - return timeString; -}; - -/** - * A view to display information about the current media item and playback - * progress. - * - * @constructor - * @param {!Player} player The player of which to display the - * playback info. - * @param {string} viewId The id of the playback info view. - */ -const PlaybackInfoView = function (player, viewId) { - /** @const @private {!Player} */ - this.player_ = player; - /** @const @private {?Element} */ - this.container_ = document.getElementById(viewId); - /** @const @private {?Element} */ - this.elapsedTimeBar_ = document.getElementById('exo_elapsed_time'); - /** @const @private {?Element} */ - this.elapsedTimeLabel_ = document.getElementById('exo_elapsed_time_label'); - /** @const @private {?Element} */ - this.durationLabel_ = document.getElementById('exo_duration_label'); - /** @const @private {!Timeout} */ - this.hideTimeout_ = new Timeout(); - /** @const @private {!Timeout} */ - this.updateTimeout_ = new Timeout(); - /** @private {boolean} */ - this.wasPlaying_ = player.getPlayWhenReady() - && player.getPlaybackState() === Player.PlaybackState.READY; - /** @private {number} */ - this.showTimeoutMs_ = SHOW_TIMEOUT_MS; - /** @private {number} */ - this.showTimeoutMsVideo_ = this.showTimeoutMs_; - - if (this.wasPlaying_) { - this.hideAfterTimeout(); - } else { - this.show(); - } - - player.addPlayerListener((playerState) => { - if (this.container_ === null) { - return; - } - const playbackPosition = playerState.playbackPosition; - const discontinuityReason = - playbackPosition ? playbackPosition.discontinuityReason : null; - if (discontinuityReason) { - const currentMediaItem = player.getCurrentMediaItem(); - this.showTimeoutMs_ = - currentMediaItem && currentMediaItem.mimeType === 'audio/*' ? - SHOW_TIMEOUT_MS_AUDIO : - this.showTimeoutMsVideo_; - } - const playWhenReady = playerState.playWhenReady; - const state = playerState.playbackState; - const isPlaying = playWhenReady && state === Player.PlaybackState.READY; - const userSeekedInBufferedRange = - discontinuityReason === Player.DiscontinuityReason.SEEK && isPlaying; - if (!isPlaying) { - this.show(); - } else if ((!this.wasPlaying_ && isPlaying) || userSeekedInBufferedRange) { - this.hideAfterTimeout(); - } - this.wasPlaying_ = isPlaying; - }); -}; - -/** Shows the player info view. */ -PlaybackInfoView.prototype.show = function () { - if (this.container_ != null) { - this.hideTimeout_.cancel(); - this.updateUi_(); - this.container_.style.display = 'block'; - this.startUpdateTimeout_(); - } -}; - -/** Hides the player info view. */ -PlaybackInfoView.prototype.hideAfterTimeout = function() { - if (this.container_ === null) { - return; - } - this.show(); - this.hideTimeout_.postDelayed(this.showTimeoutMs_).then(() => { - this.container_.style.display = 'none'; - this.updateTimeout_.cancel(); - }); -}; - -/** - * Sets the playback info view timeout. The playback info view is automatically - * hidden after this duration of time has elapsed without show() being called - * again. When playing streams with content type 'audio/*' the view is always - * displayed. - * - * @param {number} showTimeoutMs The duration in milliseconds. A non-positive - * value will cause the view to remain visible indefinitely. - */ -PlaybackInfoView.prototype.setShowTimeoutMs = function(showTimeoutMs) { - this.showTimeoutMs_ = showTimeoutMs; - this.showTimeoutMsVideo_ = showTimeoutMs; -}; - -/** - * Updates all UI components. - * - * @private - */ -PlaybackInfoView.prototype.updateUi_ = function () { - const elapsedTimeMs = this.player_.getCurrentPositionMs(); - const durationMs = this.player_.getDurationMs(); - if (this.elapsedTimeLabel_ !== null) { - this.updateDuration_(this.elapsedTimeLabel_, elapsedTimeMs, false); - } - if (this.durationLabel_ !== null) { - this.updateDuration_(this.durationLabel_, durationMs, true); - } - if (this.elapsedTimeBar_ !== null) { - this.updateProgressBar_(elapsedTimeMs, durationMs); - } -}; - -/** - * Adjust the progress bar indicating the elapsed time relative to the duration. - * - * @private - * @param {number} elapsedTimeMs The elapsed time in milliseconds. - * @param {number} durationMs The duration in milliseconds. - */ -PlaybackInfoView.prototype.updateProgressBar_ = - function(elapsedTimeMs, durationMs) { - if (elapsedTimeMs <= 0 || durationMs <= 0) { - this.elapsedTimeBar_.style.width = 0; - } else { - const widthPercentage = elapsedTimeMs / durationMs * 100; - this.elapsedTimeBar_.style.width = Math.min(100, widthPercentage) + '%'; - } -}; - -/** - * Updates the display value of the duration in the DOM formatted as hh:mm:ss. - * - * @private - * @param {!Element} element The element to update. - * @param {number} durationMs The duration in milliseconds. - * @param {boolean} hideZero If true values of zero and below are not displayed. - */ -PlaybackInfoView.prototype.updateDuration_ = - function (element, durationMs, hideZero) { - while (element.firstChild) { - element.removeChild(element.firstChild); - } - if (durationMs <= 0 && !hideZero) { - element.appendChild(dom.createDom(dom.TagName.SPAN, {}, - formatTimestampMsAsString(0))); - } else if (durationMs > 0) { - element.appendChild(dom.createDom(dom.TagName.SPAN, {}, - formatTimestampMsAsString(durationMs))); - } -}; - -/** - * Starts a repeating timeout that updates the UI every UPDATE_TIMEOUT_MS - * milliseconds. - * - * @private - */ -PlaybackInfoView.prototype.startUpdateTimeout_ = function() { - this.updateTimeout_.cancel(); - if (!this.player_.getPlayWhenReady() || - this.player_.getPlaybackState() !== Player.PlaybackState.READY) { - return; - } - this.updateTimeout_.postDelayed(UPDATE_TIMEOUT_MS).then(() => { - this.updateUi_(); - this.startUpdateTimeout_(); - }); -}; - -exports = PlaybackInfoView; diff --git a/cast_receiver_app/src/player.js b/cast_receiver_app/src/player.js deleted file mode 100644 index d7ffc58f4c..0000000000 --- a/cast_receiver_app/src/player.js +++ /dev/null @@ -1,1522 +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. - */ - -goog.module('exoplayer.cast.Player'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); -const ShakaError = goog.require('shaka.util.Error'); -const ShakaPlayer = goog.require('shaka.Player'); -const asserts = goog.require('goog.dom.asserts'); -const googArray = goog.require('goog.array'); -const safedom = goog.require('goog.dom.safe'); -const {ErrorMessages, ErrorCategory, PlaybackType, RepeatMode, getPlaybackType, UNKNOWN_ERROR} = goog.require('exoplayer.cast.constants'); -const {UuidComparator, createUuidComparator, log} = goog.require('exoplayer.cast.util'); -const {assert, fail} = goog.require('goog.asserts'); -const {clamp} = goog.require('goog.math'); - -/** - * Value indicating that no window index is currently set. - */ -const INDEX_UNSET = -1; - -/** - * Estimated time for processing the manifest after download in millisecconds. - * - * See: https://github.com/google/shaka-player/issues/1734 - */ -const MANIFEST_PROCESSING_ESTIMATE_MS = 350; - -/** - * Media element events to listen to. - * - * @enum {string} - */ -const MediaElementEvent = { - ERROR: 'error', - LOADED_DATA: 'loadeddata', - PAUSE: 'pause', - PLAYING: 'playing', - SEEKED: 'seeked', - SEEKING: 'seeking', - WAITING: 'waiting', -}; - -/** - * Shaka events to listen to. - * - * @enum {string} - */ -const ShakaEvent = { - ERROR: 'error', - STREAMING: 'streaming', - TRACKS_CHANGED: 'trackschanged', -}; - -/** - * ExoPlayer's playback states. - * - * @enum {string} - */ -const PlaybackState = { - IDLE: 'IDLE', - BUFFERING: 'BUFFERING', - READY: 'READY', - ENDED: 'ENDED', -}; - -/** - * ExoPlayer's position discontinuity reasons. - * - * @enum {string} - */ -const DiscontinuityReason = { - PERIOD_TRANSITION: 'PERIOD_TRANSITION', - SEEK: 'SEEK', -}; - -/** - * A dummy `MediaIteminfo` to be used while the actual period is not - * yet available. - * - * @const - * @type {!MediaItemInfo} - */ -const DUMMY_MEDIA_ITEM_INFO = Object.freeze({ - isSeekable: false, - isDynamic: true, - positionInFirstPeriodUs: 0, - defaultStartPositionUs: 0, - windowDurationUs: 0, - periods: [{ - id: 1, - durationUs: 0, - }], -}); - -/** - * The Player wraps a Shaka player and maintains a queue of media items. - * - * After construction the player is in `IDLE` state. Calling `#prepare` prepares - * the player with the queue item at the given window index and position. The - * state transitions to `BUFFERING`. When 'playWhenReady' is set to `true` - * playback start when the player becomes 'READY'. - * - * When the player needs to rebuffer the state goes to 'BUFFERING' and becomes - * 'READY' again when playback can be resumed. - * - * The state transitions to `ENDED` when playback reached the end of the last - * item in the queue, when the last item has been removed from the queue if - * `!IDLE`, or when `prepare` is called with an empty queue. Seeking makes the - * player transition away from `ENDED` again. - * - * When `#stop` is called or when a fatal playback error occurs, the player - * transition to `IDLE` state and needs to be prepared again to resume playback. - * - * `playWhenReady`, `repeatMode`, `shuffleModeEnabled` can be manipulated in any - * state, just as media items can be added, moved and removed. - * - * @constructor - * @param {!ShakaPlayer} shakaPlayer The shaka player to wrap. - * @param {!ConfigurationFactory} configurationFactory A factory to create a - * configuration for the Shaka player. - */ -const Player = function(shakaPlayer, configurationFactory) { - /** @private @const {?HTMLMediaElement} */ - this.videoElement_ = shakaPlayer.getMediaElement(); - /** @private @const {!ConfigurationFactory} */ - this.configurationFactory_ = configurationFactory; - /** @private @const {!Array} */ - this.playerListeners_ = []; - /** - * @private - * @const - * {?function(NetworkingEngine.RequestType, (?|null))} - */ - this.manifestResponseFilter_ = (type, response) => { - if (type === NetworkingEngine.RequestType.MANIFEST) { - setTimeout(() => { - this.updateWindowMediaItemInfo_(); - this.invalidate(); - }, MANIFEST_PROCESSING_ESTIMATE_MS); - } - }; - - /** @private {!ShakaPlayer} */ - this.shakaPlayer_ = shakaPlayer; - /** @private {boolean} */ - this.playWhenReady_ = false; - /** @private {boolean} */ - this.shuffleModeEnabled_ = false; - /** @private {!RepeatMode} */ - this.repeatMode_ = RepeatMode.OFF; - /** @private {!TrackSelectionParameters} */ - this.trackSelectionParameters_ = /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: '', - preferredTextLanguage: '', - disabledTextTrackSelectionFlags: [], - selectUndeterminedTextLanguage: false, - }); - /** @private {number} */ - this.windowIndex_ = INDEX_UNSET; - /** @private {!Array} */ - this.queue_ = []; - /** @private {!Object} */ - this.queueUuidIndexMap_ = {}; - /** @private {!UuidComparator} */ - this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); - - /** @private {!PlaybackState} */ - this.playbackState_ = PlaybackState.IDLE; - /** @private {!MediaItemInfo} */ - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - /** @private {number} */ - this.windowPeriodIndex_ = 0; - /** @private {!Object} */ - this.mediaItemInfoMap_ = {}; - /** @private {?PlayerError} */ - this.playbackError_ = null; - /** @private {?DiscontinuityReason} */ - this.discontinuityReason_ = null; - /** @private {!Array} */ - this.shuffleOrder_ = []; - /** @private {number} */ - this.shuffleIndex_ = 0; - /** @private {!PlaybackType} */ - this.playbackType_ = PlaybackType.UNKNOWN; - /** @private {boolean} */ - this.isManifestFilterRegistered_ = false; - /** @private {?string} */ - this.uuidToPrepare_ = null; - - if (!this.shakaPlayer_ || !this.videoElement_) { - throw new Error('an instance of Shaka player with a media element ' + - 'attached to it needs to be passed to the constructor.'); - } - - /** @private @const {function(!Event)} */ - this.playbackStateListener_ = (ev) => { - log(['handle event: ', ev.type]); - let invalid = false; - switch (ev.type) { - case ShakaEvent.STREAMING: { - // Arrives once after prepare when the manifest is available. - const uuid = this.queue_[this.windowIndex_].uuid; - const cachedMediaItemInfo = this.mediaItemInfoMap_[uuid]; - if (!cachedMediaItemInfo || cachedMediaItemInfo.isDynamic) { - this.updateWindowMediaItemInfo_(); - if (this.windowMediaItemInfo_.isDynamic) { - this.registerManifestResponseFilter_(); - } - invalid = true; - } - break; - } - case ShakaEvent.TRACKS_CHANGED: { - // Arrives when tracks have changed either initially or at a period - // boundary. - const periods = this.windowMediaItemInfo_.periods; - const previousPeriodIndex = this.windowPeriodIndex_; - this.evaluateAndSetCurrentPeriod_(periods); - invalid = previousPeriodIndex !== this.windowPeriodIndex_; - if (periods.length && this.windowPeriodIndex_ > 0) { - // Player transitions to next period in multiperiod stream. - this.discontinuityReason_ = this.discontinuityReason_ || - DiscontinuityReason.PERIOD_TRANSITION; - invalid = true; - } - if (this.videoElement_.paused && this.playWhenReady_) { - this.videoElement_.play(); - } - break; - } - case MediaElementEvent.LOADED_DATA: { - // Arrives once when the first frame has been rendered. - if (this.playbackType_ === PlaybackType.VIDEO_ELEMENT) { - const uuid = this.queue_[this.windowIndex_].uuid; - let mediaItemInfo = this.mediaItemInfoMap_[uuid]; - if (!mediaItemInfo || mediaItemInfo.isDynamic) { - mediaItemInfo = this.buildMediaItemInfoFromElement_(); - if (mediaItemInfo !== null) { - this.mediaItemInfoMap_[uuid] = mediaItemInfo; - this.windowMediaItemInfo_ = mediaItemInfo; - } - } - this.evaluateAndSetCurrentPeriod_(mediaItemInfo.periods); - invalid = true; - } - if (this.videoElement_.paused && this.playWhenReady_) { - // Restart after automatic skip to next queue item. - this.videoElement_.play(); - } else if (this.videoElement_.paused) { - // If paused, the PLAYING event will not be fired, hence we transition - // to state READY right here. - this.playbackState_ = PlaybackState.READY; - invalid = true; - } - break; - } - case MediaElementEvent.WAITING: - case MediaElementEvent.SEEKING: { - // Arrives at a user seek or when re-buffering starts. - if (this.playbackState_ !== PlaybackState.BUFFERING) { - this.playbackState_ = PlaybackState.BUFFERING; - invalid = true; - } - break; - } - case MediaElementEvent.PLAYING: - case MediaElementEvent.SEEKED: { - // Arrives at the end of a user seek or after re-buffering. - if (this.playbackState_ !== PlaybackState.READY) { - this.playbackState_ = PlaybackState.READY; - invalid = true; - } - break; - } - case MediaElementEvent.PAUSE: { - // Detects end of media and either skips to next or transitions to ended - // state. - if (this.videoElement_.ended) { - let nextWindowIndex = this.getNextWindowIndex(); - if (nextWindowIndex !== INDEX_UNSET) { - this.seekToWindowInternal_(nextWindowIndex, undefined); - } else { - this.playbackState_ = PlaybackState.ENDED; - invalid = true; - } - } - break; - } - } - if (invalid) { - this.invalidate(); - } - }; - /** @private @const {function(!Event)} */ - this.mediaElementErrorHandler_ = (ev) => { - console.error('Media element error reported in handler'); - this.playbackError_ = !this.videoElement_.error ? UNKNOWN_ERROR : { - message: this.videoElement_.error.message, - code: this.videoElement_.error.code, - category: ErrorCategory.MEDIA_ELEMENT, - }; - this.playbackState_ = PlaybackState.IDLE; - this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null; - this.invalidate(); - }; - /** @private @const {function(!Event)} */ - this.shakaErrorHandler_ = (ev) => { - const shakaError = /** @type {!ShakaError} */ (ev['detail']); - if (shakaError.severity !== ShakaError.Severity.RECOVERABLE) { - this.fatalShakaError_(shakaError, 'Shaka error reported by error event'); - this.invalidate(); - } else { - console.error('Recoverable Shaka error reported in handler'); - } - }; - - this.shakaPlayer_.addEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.addEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - - this.videoElement_.addEventListener( - MediaElementEvent.LOADED_DATA, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.WAITING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.PLAYING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.PAUSE, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.SEEKING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.SEEKED, this.playbackStateListener_); - - // Attach error handlers. - this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); - this.videoElement_.addEventListener( - MediaElementEvent.ERROR, this.mediaElementErrorHandler_); -}; - -/** - * Adds a listener to the player. - * - * @param {function(!PlayerState)} listener The player listener. - */ -Player.prototype.addPlayerListener = function(listener) { - this.playerListeners_.push(listener); -}; - -/** - * Removes a listener. - * - * @param {function(!Object)} listener The player listener. - */ -Player.prototype.removePlayerListener = function(listener) { - for (let i = 0; i < this.playerListeners_.length; i++) { - if (this.playerListeners_[i] === listener) { - this.playerListeners_.splice(i, 1); - break; - } - } -}; - -/** - * Gets the current PlayerState. - * - * @return {!PlayerState} - */ -Player.prototype.getPlayerState = function() { - return this.buildPlayerState_(); -}; - -/** - * Sends the current playback state to clients. - */ -Player.prototype.invalidate = function() { - const playbackState = this.buildPlayerState_(); - for (let i = 0; i < this.playerListeners_.length; i++) { - this.playerListeners_[i](playbackState); - } -}; - -/** - * Get the audio tracks. - * - * @return {!Array} An array with the track names}. - */ -Player.prototype.getAudioTracks = function() { - return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? - this.shakaPlayer_.getAudioLanguages() : - []; -}; - -/** - * Gets the video tracks. - * - * @return {!Array} An array with the video tracks. - */ -Player.prototype.getVideoTracks = function() { - return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? - this.shakaPlayer_.getVariantTracks() : - []; -}; - -/** - * Gets the playback state. - * - * @return {!PlaybackState} The playback state. - */ -Player.prototype.getPlaybackState = function() { - return this.playbackState_; -}; - -/** - * Gets the playback error if any. - * - * @return {?Object} The playback error. - */ -Player.prototype.getPlaybackError = function() { - return this.playbackError_; -}; - -/** - * Gets the duration in milliseconds or a negative value if unknown. - * - * @return {number} The duration in milliseconds. - */ -Player.prototype.getDurationMs = function() { - return this.windowMediaItemInfo_ ? - this.windowMediaItemInfo_.windowDurationUs / 1000 : -1; -}; - -/** - * Gets the current position in milliseconds or a negative value if not known. - * - * @return {number} The current position in milliseconds. - */ -Player.prototype.getCurrentPositionMs = function() { - if (!this.videoElement_.currentTime) { - return 0; - } - return (this.videoElement_.currentTime * 1000) - - (this.windowMediaItemInfo_.positionInFirstPeriodUs / 1000); -}; - -/** - * Gets the current window index. - * - * @return {number} The current window index. - */ -Player.prototype.getCurrentWindowIndex = function() { - if (this.playbackState_ === PlaybackState.IDLE) { - return this.queueUuidIndexMap_[this.uuidToPrepare_ || ''] || 0; - } - return Math.max(0, this.windowIndex_); -}; - -/** - * Gets the media item of the current window or null if the queue is empty. - * - * @return {?MediaItem} The media item of the current window. - */ -Player.prototype.getCurrentMediaItem = function() { - return this.windowIndex_ >= 0 ? this.queue_[this.windowIndex_] : null; -}; - -/** - * Gets the media item info of the current window index or null if not yet - * available. - * - * @return {?MediaItemInfo} The current media item info or undefined. - */ -Player.prototype.getCurrentMediaItemInfo = function () { - return this.windowMediaItemInfo_; -}; - -/** - * Gets the text tracks. - * - * @return {!TextTrackList} The text tracks. - */ -Player.prototype.getTextTracks = function() { - return this.videoElement_.textTracks; -}; - -/** - * Gets whether the player should play when ready. - * - * @return {boolean} True when it plays when ready. - */ -Player.prototype.getPlayWhenReady = function() { - return this.playWhenReady_; -}; - -/** - * Sets whether to play when ready. - * - * @param {boolean} playWhenReady Whether to play when ready. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setPlayWhenReady = function(playWhenReady) { - if (this.playWhenReady_ === playWhenReady) { - return false; - } - this.playWhenReady_ = playWhenReady; - this.invalidate(); - if (this.playbackState_ === PlaybackState.IDLE || - this.playbackState_ === PlaybackState.ENDED) { - return true; - } - if (this.playWhenReady_) { - this.videoElement_.play(); - } else { - this.videoElement_.pause(); - } - return true; -}; - -/** - * Gets the repeat mode. - * - * @return {!RepeatMode} The repeat mode. - */ -Player.prototype.getRepeatMode = function() { - return this.repeatMode_; -}; - -/** - * Sets the repeat mode. Must be a value of the enum Player.RepeatMode. - * - * @param {!RepeatMode} mode The repeat mode. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setRepeatMode = function(mode) { - if (this.repeatMode_ === mode) { - return false; - } - if (mode === Player.RepeatMode.OFF || - mode === Player.RepeatMode.ONE || - mode === Player.RepeatMode.ALL) { - this.repeatMode_ = mode; - } else { - throw new Error('illegal repeat mode: ' + mode); - } - this.invalidate(); - return true; -}; - -/** - * Enables or disables the shuffle mode. - * - * @param {boolean} enabled Whether the shuffle mode is enabled or not. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setShuffleModeEnabled = function(enabled) { - if (this.shuffleModeEnabled_ === enabled) { - return false; - } - this.shuffleModeEnabled_ = enabled; - this.invalidate(); - return true; -}; - -/** - * Sets the track selection parameters. - * - * @param {!TrackSelectionParameters} trackSelectionParameters The parameters. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setTrackSelectionParameters = function( - trackSelectionParameters) { - this.trackSelectionParameters_ = trackSelectionParameters; - /** @type {!PlayerConfiguration} */ - const configuration = /** @type {!PlayerConfiguration} */ ({}); - this.configurationFactory_.mapLanguageConfiguration( - trackSelectionParameters, configuration); - /** @type {!PlayerConfiguration} */ - const currentConfiguration = this.shakaPlayer_.getConfiguration(); - /** @type {boolean} */ - let isStateChange = false; - if (currentConfiguration.preferredAudioLanguage !== - configuration.preferredAudioLanguage) { - this.shakaPlayer_.selectAudioLanguage(configuration.preferredAudioLanguage); - isStateChange = true; - } - if (currentConfiguration.preferredTextLanguage !== - configuration.preferredTextLanguage) { - this.shakaPlayer_.selectTextLanguage(configuration.preferredTextLanguage); - isStateChange = true; - } - return isStateChange; -}; - -/** - * Gets the previous window index or a negative number if no item previous to - * the current item is available. - * - * @return {number} The previous window index or a negative number if the - * current item is the first item. - */ -Player.prototype.getPreviousWindowIndex = function() { - if (this.playbackType_ === PlaybackType.UNKNOWN) { - return INDEX_UNSET; - } - switch (this.repeatMode_) { - case RepeatMode.ONE: - return this.windowIndex_; - case RepeatMode.ALL: - if (this.shuffleModeEnabled_) { - const previousIndex = this.shuffleIndex_ > 0 ? - this.shuffleIndex_ - 1 : this.queue_.length - 1; - return this.shuffleOrder_[previousIndex]; - } else { - const previousIndex = this.windowIndex_ > 0 ? - this.windowIndex_ - 1 : this.queue_.length - 1; - return previousIndex; - } - break; - case RepeatMode.OFF: - if (this.shuffleModeEnabled_) { - const previousIndex = this.shuffleIndex_ - 1; - return previousIndex < 0 ? -1 : this.shuffleOrder_[previousIndex]; - } else { - const previousIndex = this.windowIndex_ - 1; - return previousIndex < 0 ? -1 : previousIndex; - } - break; - default: - throw new Error('illegal state of repeat mode: ' + this.repeatMode_); - } -}; - -/** - * Gets the next window index or a negative number if the current item is the - * last item. - * - * @return {number} The next window index or a negative number if the current - * item is the last item. - */ -Player.prototype.getNextWindowIndex = function() { - if (this.playbackType_ === PlaybackType.UNKNOWN) { - return INDEX_UNSET; - } - switch (this.repeatMode_) { - case RepeatMode.ONE: - return this.windowIndex_; - case RepeatMode.ALL: - if (this.shuffleModeEnabled_) { - const nextIndex = (this.shuffleIndex_ + 1) % this.queue_.length; - return this.shuffleOrder_[nextIndex]; - } else { - return (this.windowIndex_ + 1) % this.queue_.length; - } - break; - case RepeatMode.OFF: - if (this.shuffleModeEnabled_) { - const nextIndex = this.shuffleIndex_ + 1; - return nextIndex < this.shuffleOrder_.length ? - this.shuffleOrder_[nextIndex] : -1; - } else { - const nextIndex = this.windowIndex_ + 1; - return nextIndex < this.queue_.length ? nextIndex : -1; - } - break; - default: - throw new Error('illegal state of repeat mode: ' + this.repeatMode_); - } -}; - -/** - * Gets whether the current window is seekable. - * - * @return {boolean} True if seekable. - */ -Player.prototype.isCurrentWindowSeekable = function() { - return !!this.videoElement_.seekable; -}; - -/** - * Seeks to the positionMs of the media item with the given uuid. - * - * @param {string} uuid The uuid of the media item to seek to. - * @param {number|undefined} positionMs The position in milliseconds to seek to. - * @return {boolean} True if a seek operation has been processed, false - * otherwise. - */ -Player.prototype.seekToUuid = function(uuid, positionMs) { - if (this.playbackState_ === PlaybackState.IDLE) { - this.uuidToPrepare_ = uuid; - this.videoElement_.currentTime = - this.getPosition_(positionMs, INDEX_UNSET) / 1000; - this.invalidate(); - return true; - } - const windowIndex = this.queueUuidIndexMap_[uuid]; - if (windowIndex !== undefined) { - positionMs = this.getPosition_(positionMs, windowIndex); - this.discontinuityReason_ = DiscontinuityReason.SEEK; - this.seekToWindowInternal_(windowIndex, positionMs); - return true; - } - return false; -}; - -/** - * Seeks to the positionMs of the given window. - * - * The index must be a valid index of the current queue, else this method does - * nothing. - * - * @param {number} windowIndex The index of the window to seek to. - * @param {number|undefined} positionMs The position to seek to within the - * window. - */ -Player.prototype.seekToWindow = function(windowIndex, positionMs) { - if (windowIndex < 0 || windowIndex >= this.queue_.length) { - return; - } - this.seekToUuid(this.queue_[windowIndex].uuid, positionMs); -}; - -/** - * Gets the number of media items in the queue. - * - * @return {number} The size of the queue. - */ -Player.prototype.getQueueSize = function() { - return this.queue_.length; -}; - -/** - * Adds an array of items at the given index of the queue. - * - * Items are expected to have been validated with `validation#validateMediaItem` - * or `validation#validateMediaItems` before being passed to this method. - * - * @param {number} index The index where to insert the media item. - * @param {!Array} mediaItems The media items. - * @param {!Array|undefined} shuffleOrder The new shuffle order. - * @return {number} The number of added items. - */ -Player.prototype.addQueueItems = function(index, mediaItems, shuffleOrder) { - if (index < 0 || mediaItems.length === 0) { - return 0; - } - let addedItemCount = 0; - index = Math.min(this.queue_.length, index); - mediaItems.forEach((itemToAdd) => { - if (this.queueUuidIndexMap_[itemToAdd.uuid] === undefined) { - this.queue_.splice(index + addedItemCount, 0, itemToAdd); - this.queueUuidIndexMap_[itemToAdd.uuid] = index + addedItemCount; - addedItemCount++; - } - }); - if (addedItemCount === 0) { - return 0; - } - this.buildUuidIndexMap_(index + addedItemCount); - this.setShuffleOrder_(shuffleOrder); - if (this.queue_.length === addedItemCount) { - this.windowIndex_ = 0; - this.updateShuffleIndex_(); - } else if ( - index <= this.windowIndex_ && - this.playbackType_ !== PlaybackType.UNKNOWN) { - this.windowIndex_ += mediaItems.length; - this.updateShuffleIndex_(); - } - this.invalidate(); - return addedItemCount; -}; - -/** - * Removes the queue items with the given uuids. - * - * @param {!Array} uuids The uuids of the queue items to remove. - * @return {number} The number of items removed from the queue. - */ -Player.prototype.removeQueueItems = function(uuids) { - let currentWindowRemoved = false; - let lowestIndexRemoved = this.queue_.length - 1; - const initialQueueSize = this.queue_.length; - // Sort in descending order to start removing from the end. - uuids = uuids.sort(this.uuidComparator_); - uuids.forEach((uuid) => { - const indexToRemove = this.queueUuidIndexMap_[uuid]; - if (indexToRemove === undefined) { - return; - } - // Remove the item from the queue. - this.queue_.splice(indexToRemove, 1); - // Remove the corresponding media item info. - delete this.mediaItemInfoMap_[uuid]; - // Remove the mapping to the window index. - delete this.queueUuidIndexMap_[uuid]; - lowestIndexRemoved = Math.min(lowestIndexRemoved, indexToRemove); - currentWindowRemoved = - currentWindowRemoved || indexToRemove === this.windowIndex_; - // The window index needs to be decreased when the item which has been - // removed was before the current item, when the current item at the last - // position has been removed, or when the queue has been emptied. - if (indexToRemove < this.windowIndex_ || - (indexToRemove === this.windowIndex_ && - indexToRemove === this.queue_.length) || - this.queue_.length === 0) { - this.windowIndex_--; - } - // Adjust the shuffle order. - let shuffleIndexToRemove; - this.shuffleOrder_.forEach((windowIndex, index) => { - if (windowIndex > indexToRemove) { - // Decrease the index in the shuffle order. - this.shuffleOrder_[index]--; - } else if (windowIndex === indexToRemove) { - // Recall index for removal after traversing. - shuffleIndexToRemove = index; - } - }); - // Remove the shuffle order entry of the removed item. - this.shuffleOrder_.splice(shuffleIndexToRemove, 1); - }); - const removedItemsCount = initialQueueSize - this.queue_.length; - if (removedItemsCount === 0) { - return 0; - } - this.updateShuffleIndex_(); - this.buildUuidIndexMap_(lowestIndexRemoved); - if (currentWindowRemoved) { - if (this.queue_.length === 0) { - this.playbackState_ = this.playbackState_ === PlaybackState.IDLE ? - PlaybackState.IDLE : - PlaybackState.ENDED; - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.windowPeriodIndex_ = 0; - this.videoElement_.currentTime = 0; - this.uuidToPrepare_ = null; - this.unregisterManifestResponseFilter_(); - this.unload_(/** reinitialiseMediaSource= */ true); - } else if (this.windowIndex_ >= 0) { - const windowIndexToPrepare = this.windowIndex_; - this.windowIndex_ = INDEX_UNSET; - this.seekToWindowInternal_(windowIndexToPrepare, undefined); - return removedItemsCount; - } - } - this.invalidate(); - return removedItemsCount; -}; - -/** - * Move the queue item with the given id to the given position. - * - * @param {string} uuid The uuid of the queue item to move. - * @param {number} to The position to move the item to. - * @param {!Array|undefined} shuffleOrder The new shuffle order. - * @return {boolean} Whether the item has been moved. - */ -Player.prototype.moveQueueItem = function(uuid, to, shuffleOrder) { - if (to < 0 || to >= this.queue_.length) { - return false; - } - const windowIndex = this.queueUuidIndexMap_[uuid]; - if (windowIndex === undefined) { - return false; - } - const itemMoved = this.moveInQueue_(windowIndex, to); - if (itemMoved) { - this.setShuffleOrder_(shuffleOrder); - this.invalidate(); - } - return itemMoved; -}; - -/** - * Prepares the player at the current window index and position. - * - * The playback state immediately transitions to `BUFFERING`. If the queue - * is empty the player transitions to `ENDED`. - */ -Player.prototype.prepare = function() { - if (this.queue_.length === 0) { - this.uuidToPrepare_ = null; - this.playbackState_ = PlaybackState.ENDED; - this.invalidate(); - return; - } - if (this.uuidToPrepare_) { - this.windowIndex_ = - this.queueUuidIndexMap_[this.uuidToPrepare_] || INDEX_UNSET; - this.uuidToPrepare_ = null; - } - this.windowIndex_ = clamp(this.windowIndex_, 0, this.queue_.length - 1); - this.prepare_(this.getCurrentPositionMs()); - this.invalidate(); -}; - -/** - * Stops the player. - * - * Calling this method causes the player to transition into `IDLE` state. - * If `reset` is `true` the player is reset to the initial state of right - * after construction. If `reset` is `false`, the media queue is preserved - * and calling `prepare()` results in resuming the player state to what it - * was before calling `#stop(false)`. - * - * @param {boolean} reset Whether the state should be reset. - * @return {!Promise} A promise which resolves after async unload - * tasks have finished. - */ -Player.prototype.stop = function(reset) { - this.playbackState_ = PlaybackState.IDLE; - this.playbackError_ = null; - this.discontinuityReason_ = null; - this.unregisterManifestResponseFilter_(); - this.uuidToPrepare_ = this.uuidToPrepare_ || (this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null); - if (reset) { - this.uuidToPrepare_ = null; - this.queue_ = []; - this.queueUuidIndexMap_ = {}; - this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); - this.windowIndex_ = INDEX_UNSET; - this.mediaItemInfoMap_ = {}; - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.windowPeriodIndex_ = 0; - this.videoElement_.currentTime = 0; - this.shuffleOrder_ = []; - this.shuffleIndex_ = 0; - } - this.invalidate(); - return this.unload_(/** reinitialiseMediaSource= */ !reset); -}; - -/** - * Resets player and media element. - * - * @private - * @param {boolean} reinitialiseMediaSource Whether the media source should be - * reinitialized. - * @return {!Promise} A promise which resolves after async unload - * tasks have finished. - */ -Player.prototype.unload_ = function(reinitialiseMediaSource) { - const playbackTypeToUnload = this.playbackType_; - this.playbackType_ = PlaybackType.UNKNOWN; - switch (playbackTypeToUnload) { - case PlaybackType.VIDEO_ELEMENT: - this.videoElement_.removeAttribute('src'); - this.videoElement_.load(); - return Promise.resolve(); - case PlaybackType.SHAKA_PLAYER: - return new Promise((resolve, reject) => { - this.shakaPlayer_.unload(reinitialiseMediaSource) - .then(resolve) - .catch(resolve); - }); - default: - return Promise.resolve(); - } -}; - -/** - * Releases the current Shaka instance and create a new one. - * - * This function should only be called if the Shaka instance is out of order due - * to https://github.com/google/shaka-player/issues/1785. It assumes the current - * Shaka instance has fallen into a state in which promises returned by - * `shakaPlayer.load` and `shakaPlayer.unload` do not resolve nor are they - * rejected anymore. - * - * @private - */ -Player.prototype.replaceShaka_ = function() { - // Remove all listeners. - this.shakaPlayer_.removeEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.removeEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - this.shakaPlayer_.removeEventListener( - ShakaEvent.ERROR, this.shakaErrorHandler_); - // Unregister response filter if any. - this.unregisterManifestResponseFilter_(); - // Unload the old instance. - this.shakaPlayer_.unload(false); - // Reset video element. - this.videoElement_.removeAttribute('src'); - this.videoElement_.load(); - // Create a new instance and add listeners. - this.shakaPlayer_ = new ShakaPlayer(this.videoElement_); - this.shakaPlayer_.addEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.addEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); -}; - -/** - * Moves a queue item within the queue. - * - * @private - * @param {number} from The initial position. - * @param {number} to The position to move the item to. - * @return {boolean} Whether the item has been moved. - */ -Player.prototype.moveInQueue_ = function(from, to) { - if (from < 0 || to < 0 - || from >= this.queue_.length || to >= this.queue_.length - || from === to) { - return false; - } - this.queue_.splice(to, 0, this.queue_.splice(from, 1)[0]); - this.buildUuidIndexMap_(Math.min(from, to)); - if (from === this.windowIndex_) { - this.windowIndex_ = to; - } else if (from > this.windowIndex_ && to <= this.windowIndex_) { - this.windowIndex_++; - } else if (from < this.windowIndex_ && to >= this.windowIndex_) { - this.windowIndex_--; - } - return true; -}; - -/** - * Shuffles the queue. - * - * @private - */ -Player.prototype.shuffle_ = function() { - this.shuffleOrder_ = this.queue_.map((item, index) => index); - googArray.shuffle(this.shuffleOrder_); - this.updateShuffleIndex_(); -}; - -/** - * Sets the new shuffle order. - * - * @private - * @param {!Array|undefined} shuffleOrder The new shuffle order. - */ -Player.prototype.setShuffleOrder_ = function(shuffleOrder) { - if (shuffleOrder && this.queue_.length === shuffleOrder.length) { - this.shuffleOrder_ = shuffleOrder; - this.updateShuffleIndex_(); - } else if (this.shuffleOrder_.length !== this.queue_.length) { - this.shuffle_(); - } -}; - -/** - * Updates the shuffle order to point to the current window index. - * - * @private - */ -Player.prototype.updateShuffleIndex_ = function() { - this.shuffleIndex_ = - this.shuffleOrder_.findIndex((idx) => idx === this.windowIndex_); -}; - -/** - * Builds the `queueUuidIndexMap` using the uuid of a media item as the key and - * the window index as the value of an entry. - * - * @private - * @param {number} startPosition The window index to start updating at. - */ -Player.prototype.buildUuidIndexMap_ = function(startPosition) { - for (let i = startPosition; i < this.queue_.length; i++) { - this.queueUuidIndexMap_[this.queue_[i].uuid] = i; - } -}; - -/** - * Gets the default position of the current window. - * - * @private - * @return {number} The default position of the current window. - */ -Player.prototype.getDefaultPosition_ = function() { - return this.windowMediaItemInfo_.defaultStartPositionUs; -}; - -/** - * Checks whether the given position is buffered. - * - * @private - * @param {number} positionMs The position to check. - * @return {boolean} true if the media data of the current position is buffered. - */ -Player.prototype.isBuffered_ = function(positionMs) { - const ranges = this.videoElement_.buffered; - for (let i = 0; i < ranges.length; i++) { - const start = ranges.start(i) * 1000; - const end = ranges.end(i) * 1000; - if (start <= positionMs && positionMs <= end) { - return true; - } - } - return false; -}; - -/** - * Seeks to the positionMs of the given window. - * - * To signal a user seek, callers are expected to set the discontinuity reason - * to `DiscontinuityReason.SEEK` before calling this method. If not set this - * method may set the `DiscontinuityReason.PERIOD_TRANSITION` in case the - * `windowIndex` changes. - * - * @private - * @param {number} windowIndex The non-negative index of the window to seek to. - * @param {number|undefined} positionMs The position to seek to within the - * window. If undefined it seeks to the default position of the window. - */ -Player.prototype.seekToWindowInternal_ = function(windowIndex, positionMs) { - const windowChanges = this.windowIndex_ !== windowIndex; - // Update window index and position in any case. - this.windowIndex_ = Math.max(0, windowIndex); - this.updateShuffleIndex_(); - const seekPositionMs = this.getPosition_(positionMs, windowIndex); - this.videoElement_.currentTime = seekPositionMs / 1000; - - // IDLE or ENDED with empty queue. - if (this.playbackState_ === PlaybackState.IDLE || this.queue_.length === 0) { - // Do nothing but report the change in window index and position. - this.invalidate(); - return; - } - - // Prepare for a seek to another window or when in ENDED state whilst the - // queue is not empty but prepare has not been called yet. - if (windowChanges || this.playbackType_ === PlaybackType.UNKNOWN) { - // Reset and prepare. - this.unregisterManifestResponseFilter_(); - this.discontinuityReason_ = - this.discontinuityReason_ || DiscontinuityReason.PERIOD_TRANSITION; - this.prepare_(seekPositionMs); - this.invalidate(); - return; - } - - // Sync playWhenReady with video element after ENDED state. - if (this.playbackState_ === PlaybackState.ENDED && this.playWhenReady_) { - this.videoElement_.play(); - return; - } - - // A seek within the current window when READY or BUFFERING. - this.playbackState_ = this.isBuffered_(seekPositionMs) ? - PlaybackState.READY : - PlaybackState.BUFFERING; - this.invalidate(); -}; - -/** - * Prepares the player at the current window index and the given - * `startPositionMs`. - * - * Calling this method resets the media item information, transitions to - * 'BUFFERING', prepares either the plain video element for progressive - * media, or the Shaka player for adaptive media. - * - * Media items are mapped by media type to a `PlaybackType`s in - * `exoplayer.cast.constants.SupportedMediaTypes`. Unsupported mime types will - * cause the player to transition to the `IDLE` state. - * - * Items in the queue are expected to have been validated with - * `validation#validateMediaItem` or `validation#validateMediaItems`. If this is - * not the case this method might throw an Assertion exception. - * - * @private - * @param {number} startPositionMs The position at which to start playback. - * @throws {!AssertionException} In case an unvalidated item can't be mapped to - * a supported playback type. - */ -Player.prototype.prepare_ = function(startPositionMs) { - const mediaItem = this.queue_[this.windowIndex_]; - const windowUuid = this.queue_[this.windowIndex_].uuid; - const mediaItemInfo = this.mediaItemInfoMap_[windowUuid]; - if (mediaItemInfo && !mediaItemInfo.isDynamic) { - // Do reuse if not dynamic. - this.windowMediaItemInfo_ = mediaItemInfo; - } else { - // Use the dummy info until manifest/data available. - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.mediaItemInfoMap_[windowUuid] = DUMMY_MEDIA_ITEM_INFO; - } - this.windowPeriodIndex_ = 0; - this.playbackType_ = getPlaybackType(mediaItem.mimeType); - this.playbackState_ = PlaybackState.BUFFERING; - const uri = mediaItem.media.uri; - switch (this.playbackType_) { - case PlaybackType.VIDEO_ELEMENT: - this.videoElement_.currentTime = startPositionMs / 1000; - this.shakaPlayer_.unload(false) - .then(() => { - this.setMediaElementSrc(uri); - this.videoElement_.currentTime = startPositionMs / 1000; - }) - .catch((error) => { - // Let's still try. We actually don't need Shaka right now. - this.setMediaElementSrc(uri); - this.videoElement_.currentTime = startPositionMs / 1000; - console.error('Shaka error while unloading', error); - }); - break; - case PlaybackType.SHAKA_PLAYER: - this.shakaPlayer_.configure( - this.configurationFactory_.createConfiguration( - mediaItem, this.trackSelectionParameters_)); - this.shakaPlayer_.load(uri, startPositionMs / 1000).catch((error) => { - const shakaError = /** @type {!ShakaError} */ (error); - if (shakaError.severity !== ShakaError.Severity.RECOVERABLE && - shakaError.code !== ShakaError.Code.LOAD_INTERRUPTED) { - this.fatalShakaError_(shakaError, 'loading failed for uri: ' + uri); - this.invalidate(); - } else { - console.error('Recoverable Shaka error while loading', shakaError); - } - }); - break; - default: - fail('unknown playback type for mime type: ' + mediaItem.mimeType); - } -}; - -/** - * Sets the uri to the `src` attribute of the media element in a safe way. - * - * @param {string} uri The uri to set as the value of the `src` attribute. - */ -Player.prototype.setMediaElementSrc = function(uri) { - safedom.setVideoSrc( - asserts.assertIsHTMLVideoElement(this.videoElement_), uri); -}; - -/** - * Handles a fatal Shaka error by setting the playback error, transitioning to - * state `IDLE` and setting the playback type to `UNKNOWN`. Player needs to be - * reprepared after calling this method. - * - * @private - * @param {!ShakaError} shakaError The error. - * @param {string|undefined} customMessage A custom message. - */ -Player.prototype.fatalShakaError_ = function(shakaError, customMessage) { - this.playbackState_ = PlaybackState.IDLE; - this.playbackType_ = PlaybackType.UNKNOWN; - this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null; - if (typeof shakaError.severity === 'undefined') { - // Not a Shaka error. We need to assume the worst case. - this.replaceShaka_(); - this.playbackError_ = /** @type {!PlayerError} */ ({ - message: ErrorMessages.UNKNOWN_FATAL_ERROR, - code: -1, - category: ErrorCategory.FATAL_SHAKA_ERROR, - }); - } else { - // A critical ShakaError. Can be recovered from by calling prepare. - this.playbackError_ = /** @type {!PlayerError} */ ({ - message: customMessage || shakaError.message || - ErrorMessages.SHAKA_UNKNOWN_ERROR, - code: shakaError.code, - category: shakaError.category, - }); - } - console.error('caught shaka load error', shakaError); -}; - -/** - * Gets the position to use. If `undefined` or `null` is passed as argument the - * default start position of the media item info of the given windowIndex is - * returned. - * - * @private - * @param {?number|undefined} positionMs The position in milliseconds, - * `undefined` or `null`. - * @param {number} windowIndex The window index for which to evaluate the - * position. - * @return {number} The position to use in milliseconds. - */ -Player.prototype.getPosition_ = function(positionMs, windowIndex) { - if (positionMs !== undefined) { - return Math.max(0, positionMs); - } - const windowUuid = assert(this.queue_[windowIndex]).uuid; - const mediaItemInfo = - this.mediaItemInfoMap_[windowUuid] || DUMMY_MEDIA_ITEM_INFO; - return mediaItemInfo.defaultStartPositionUs; -}; - -/** - * Refreshes the media item info of the current window. - * - * @private - */ -Player.prototype.updateWindowMediaItemInfo_ = function() { - this.windowMediaItemInfo_ = this.buildMediaItemInfo_(); - if (this.windowMediaItemInfo_) { - const mediaItem = this.queue_[this.windowIndex_]; - this.mediaItemInfoMap_[mediaItem.uuid] = this.windowMediaItemInfo_; - this.evaluateAndSetCurrentPeriod_(this.windowMediaItemInfo_.periods); - } -}; - -/** - * Evaluates the current period and stores it in a member variable. - * - * @private - * @param {!Array} periods The periods of the current mediaItem. - */ -Player.prototype.evaluateAndSetCurrentPeriod_ = function(periods) { - const positionUs = this.getCurrentPositionMs() * 1000; - let positionInWindowUs = 0; - periods.some((period, i) => { - positionInWindowUs += period.durationUs; - if (positionUs < positionInWindowUs) { - this.windowPeriodIndex_ = i; - return true; - } - return false; - }); -}; - -/** - * Registers a response filter which is notified when a manifest has been - * downloaded. - * - * @private - */ -Player.prototype.registerManifestResponseFilter_ = function() { - if (this.isManifestFilterRegistered_) { - return; - } - this.shakaPlayer_.getNetworkingEngine().registerResponseFilter( - this.manifestResponseFilter_); - this.isManifestFilterRegistered_ = true; -}; - -/** - * Unregisters the manifest response filter. - * - * @private - */ -Player.prototype.unregisterManifestResponseFilter_ = function() { - if (this.isManifestFilterRegistered_) { - this.shakaPlayer_.getNetworkingEngine().unregisterResponseFilter( - this.manifestResponseFilter_); - this.isManifestFilterRegistered_ = false; - } -}; - -/** - * Builds a MediaItemInfo from the media element. - * - * @private - * @return {!MediaItemInfo} A media item info. - */ -Player.prototype.buildMediaItemInfoFromElement_ = function() { - const durationUs = this.videoElement_.duration * 1000 * 1000; - return /** @type {!MediaItemInfo} */ ({ - isSeekable: !!this.videoElement_.seekable, - isDynamic: false, - positionInFirstPeriodUs: 0, - defaultStartPositionUs: 0, - windowDurationUs: durationUs, - periods: [{ - id: 0, - durationUs: durationUs, - }], - }); -}; - -/** - * Builds a MediaItemInfo from the manifest or null if no manifest is available. - * - * @private - * @return {!MediaItemInfo} - */ -Player.prototype.buildMediaItemInfo_ = function() { - const manifest = this.shakaPlayer_.getManifest(); - if (manifest === null) { - return DUMMY_MEDIA_ITEM_INFO; - } - const timeline = manifest.presentationTimeline; - const isDynamic = timeline.isLive(); - const windowStartUs = isDynamic ? - timeline.getSeekRangeStart() * 1000 * 1000 : - timeline.getSegmentAvailabilityStart() * 1000 * 1000; - const windowDurationUs = isDynamic ? - (timeline.getSeekRangeEnd() - timeline.getSeekRangeStart()) * 1000 * - 1000 : - timeline.getDuration() * 1000 * 1000; - const defaultStartPositionUs = isDynamic ? - timeline.getSeekRangeEnd() * 1000 * 1000 : - timeline.getSegmentAvailabilityStart() * 1000 * 1000; - - const periods = []; - let previousStartTimeUs = 0; - let positionInFirstPeriodUs = 0; - manifest.periods.forEach((period, index) => { - const startTimeUs = period.startTime * 1000 * 1000; - periods.push({ - id: Math.floor(startTimeUs), - }); - if (index > 0) { - // calculate duration of previous period - periods[index - 1].durationUs = startTimeUs - previousStartTimeUs; - if (previousStartTimeUs <= windowStartUs && windowStartUs < startTimeUs) { - positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; - } - } - previousStartTimeUs = startTimeUs; - }); - // calculate duration of last period - if (periods.length) { - const lastPeriodDurationUs = - isDynamic ? Infinity : windowDurationUs - previousStartTimeUs; - periods.slice(-1)[0].durationUs = lastPeriodDurationUs; - if (previousStartTimeUs <= windowStartUs) { - positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; - } - } - return /** @type {!MediaItemInfo} */ ({ - windowDurationUs: Math.floor(windowDurationUs), - defaultStartPositionUs: Math.floor(defaultStartPositionUs), - isSeekable: this.videoElement_ ? !!this.videoElement_.seekable : false, - positionInFirstPeriodUs: Math.floor(positionInFirstPeriodUs), - isDynamic: isDynamic, - periods: periods, - }); -}; - -/** - * Builds the player state message. - * - * @private - * @return {!PlayerState} The player state. - */ -Player.prototype.buildPlayerState_ = function() { - const playerState = { - playbackState: this.getPlaybackState(), - playbackParameters: { - speed: 1, - pitch: 1, - skipSilence: false, - }, - playbackPosition: this.buildPlaybackPosition_(), - playWhenReady: this.getPlayWhenReady(), - windowIndex: this.getCurrentWindowIndex(), - windowCount: this.queue_.length, - audioTracks: this.getAudioTracks() || [], - videoTracks: this.getVideoTracks(), - repeatMode: this.repeatMode_, - shuffleModeEnabled: this.shuffleModeEnabled_, - mediaQueue: this.queue_.slice(), - mediaItemsInfo: this.mediaItemInfoMap_, - shuffleOrder: this.shuffleOrder_, - sequenceNumber: -1, - }; - if (this.playbackError_) { - playerState.error = this.playbackError_; - this.playbackError_ = null; - } - return playerState; -}; - -/** - * Builds the playback position. Returns null if all properties of the playback - * position are empty. - * - * @private - * @return {?PlaybackPosition} The playback position. - */ -Player.prototype.buildPlaybackPosition_ = function() { - if ((this.playbackState_ === PlaybackState.IDLE && !this.uuidToPrepare_) || - this.playbackState_ === PlaybackState.ENDED && this.queue_.length === 0) { - this.discontinuityReason_ = null; - return null; - } - /** @type {!PlaybackPosition} */ - const playbackPosition = { - positionMs: this.getCurrentPositionMs(), - uuid: this.uuidToPrepare_ || this.queue_[this.windowIndex_].uuid, - periodId: this.windowMediaItemInfo_.periods[this.windowPeriodIndex_].id, - discontinuityReason: null, - }; - if (this.discontinuityReason_ !== null) { - playbackPosition.discontinuityReason = this.discontinuityReason_; - this.discontinuityReason_ = null; - } - return playbackPosition; -}; - -exports = Player; -exports.RepeatMode = RepeatMode; -exports.PlaybackState = PlaybackState; -exports.DiscontinuityReason = DiscontinuityReason; -exports.DUMMY_MEDIA_ITEM_INFO = DUMMY_MEDIA_ITEM_INFO; diff --git a/cast_receiver_app/src/timeout.js b/cast_receiver_app/src/timeout.js deleted file mode 100644 index e5df5ec2f4..0000000000 --- a/cast_receiver_app/src/timeout.js +++ /dev/null @@ -1,68 +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. - */ - -goog.module('exoplayer.cast.Timeout'); - -/** - * A timeout which can be cancelled. - */ -class Timeout { - constructor() { - /** @private {?number} */ - this.timeout_ = null; - } - /** - * Returns a promise which resolves when the duration of time defined by - * delayMs has elapsed and cancel() has not been called earlier. - * - * If the timeout is already set, the former timeout is cancelled and a new - * one is started. - * - * @param {number} delayMs The delay after which to resolve or a non-positive - * value if it should never resolve. - * @return {!Promise} Resolves after the given delayMs or never - * for a non-positive delay. - */ - postDelayed(delayMs) { - this.cancel(); - return new Promise((resolve, reject) => { - if (delayMs <= 0) { - return; - } - this.timeout_ = setTimeout(() => { - if (this.timeout_) { - this.timeout_ = null; - resolve(); - } - }, delayMs); - }); - } - - /** Cancels the timeout. */ - cancel() { - if (this.timeout_) { - clearTimeout(this.timeout_); - this.timeout_ = null; - } - } - - /** @return {boolean} true if the timeout is currently ongoing. */ - isOngoing() { - return this.timeout_ !== null; - } -} - -exports = Timeout; diff --git a/cast_receiver_app/src/util.js b/cast_receiver_app/src/util.js deleted file mode 100644 index 75afd9e5d3..0000000000 --- a/cast_receiver_app/src/util.js +++ /dev/null @@ -1,62 +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. - */ - -goog.module('exoplayer.cast.util'); - -/** - * Indicates whether the logging is turned on. - */ -const enableLogging = true; - -/** - * Logs to the console if logging enabled. - * - * @param {!Array<*>} statements The log statements to be logged. - */ -const log = function(statements) { - if (enableLogging) { - console.log.apply(console, statements); - } -}; - -/** - * A comparator function for uuids. - * - * @typedef {function(string,string):number} - */ -let UuidComparator; - -/** - * Creates a comparator function which sorts uuids in descending order by the - * corresponding index of the given map. - * - * @param {!Object} uuidIndexMap The map with uuids as the key - * and the window index as the value. - * @return {!UuidComparator} The comparator for sorting. - */ -const createUuidComparator = function(uuidIndexMap) { - return (a, b) => { - const indexA = uuidIndexMap[a] || -1; - const indexB = uuidIndexMap[b] || -1; - return indexB - indexA; - }; -}; - -exports = { - log, - createUuidComparator, - UuidComparator, -}; diff --git a/cast_receiver_app/test/caf_bootstrap.js b/cast_receiver_app/test/caf_bootstrap.js deleted file mode 100644 index 721360e8a7..0000000000 --- a/cast_receiver_app/test/caf_bootstrap.js +++ /dev/null @@ -1,33 +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. - */ - -/** - * @fileoverview Declares constants which are provided by the CAF externs and - * are not included in uncompiled unit tests. - */ -cast = { - framework: { - system: { - EventType: { - SENDER_CONNECTED: 'sender_connected', - SENDER_DISCONNECTED: 'sender_disconnected', - }, - DisconnectReason: { - REQUESTED_BY_SENDER: 'requested_by_sender', - }, - }, - }, -}; diff --git a/cast_receiver_app/test/configuration_factory_test.js b/cast_receiver_app/test/configuration_factory_test.js deleted file mode 100644 index af9254c59e..0000000000 --- a/cast_receiver_app/test/configuration_factory_test.js +++ /dev/null @@ -1,86 +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. - */ - -goog.module('exoplayer.cast.test.configurationfactory'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let configurationFactory; - -testSuite({ - setUp() { - configurationFactory = new ConfigurationFactory(); - }, - - /** Tests creating the most basic configuration. */ - testCreateBasicConfiguration() { - /** @type {!TrackSelectionParameters} */ - const selectionParameters = /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: 'en', - preferredTextLanguage: 'it', - }); - const configuration = configurationFactory.createConfiguration( - util.queue.slice(0, 1), selectionParameters); - assertEquals('en', configuration.preferredAudioLanguage); - assertEquals('it', configuration.preferredTextLanguage); - // Assert empty drm configuration as default. - assertArrayEquals(['servers'], Object.keys(configuration.drm)); - assertArrayEquals([], Object.keys(configuration.drm.servers)); - }, - - /** Tests defaults for undefined audio and text languages. */ - testCreateBasicConfiguration_languagesUndefined() { - const configuration = configurationFactory.createConfiguration( - util.queue.slice(0, 1), /** @type {!TrackSelectionParameters} */ ({})); - assertEquals('', configuration.preferredAudioLanguage); - assertEquals('', configuration.preferredTextLanguage); - }, - - /** Tests creating a drm configuration */ - testCreateDrmConfiguration() { - /** @type {!MediaItem} */ - const mediaItem = util.queue[1]; - mediaItem.drmSchemes = [ - { - uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - licenseServer: { - uri: 'drm-uri0', - }, - }, - { - uuid: '9a04f079-9840-4286-ab92-e65be0885f95', - licenseServer: { - uri: 'drm-uri1', - }, - }, - { - uuid: 'unsupported-drm-uuid', - licenseServer: { - uri: 'drm-uri2', - }, - }, - ]; - const configuration = - configurationFactory.createConfiguration(mediaItem, {}); - assertEquals('drm-uri0', configuration.drm.servers['com.widevine.alpha']); - assertEquals( - 'drm-uri1', configuration.drm.servers['com.microsoft.playready']); - assertEquals(2, Object.entries(configuration.drm.servers).length); - } -}); diff --git a/cast_receiver_app/test/externs.js b/cast_receiver_app/test/externs.js deleted file mode 100644 index a90a367691..0000000000 --- a/cast_receiver_app/test/externs.js +++ /dev/null @@ -1,36 +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. - */ - -/** - * Externs for unit tests to avoid renaming of properties. - * - * These externs are only required when building with bazel because the - * closure_js_test compiles tests as well. - * - * @externs - */ - -/** @record */ -function ValidationObject() {} - -/** @type {*} */ -ValidationObject.prototype.field; - -/** @record */ -function Uuids() {} - -/** @type {!Array} */ -Uuids.prototype.uuids; diff --git a/cast_receiver_app/test/message_dispatcher_test.js b/cast_receiver_app/test/message_dispatcher_test.js deleted file mode 100644 index 3e7daaf573..0000000000 --- a/cast_receiver_app/test/message_dispatcher_test.js +++ /dev/null @@ -1,49 +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. - * - * @fileoverview Unit tests for the message dispatcher. - */ - -goog.module('exoplayer.cast.test.messagedispatcher'); -goog.setTestOnly(); - -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); - -let contextMock; -let messageDispatcher; - -testSuite({ - setUp() { - mocks.setUp(); - contextMock = mocks.createCastReceiverContextFake(); - messageDispatcher = new MessageDispatcher( - 'urn:x-cast:com.google.exoplayer.cast', contextMock); - }, - - /** Test marshalling Infinity */ - testStringifyInfinity() { - const senderId = 'sender0'; - const name = 'Federico Vespucci'; - messageDispatcher.send(senderId, {name: name, duration: Infinity}); - - const msg = mocks.state().outputMessages[senderId][0]; - assertUndefined(msg.duration); - assertFalse(msg.hasOwnProperty('duration')); - assertEquals(name, msg.name); - assertTrue(msg.hasOwnProperty('name')); - } -}); diff --git a/cast_receiver_app/test/mocks.js b/cast_receiver_app/test/mocks.js deleted file mode 100644 index 244ac72829..0000000000 --- a/cast_receiver_app/test/mocks.js +++ /dev/null @@ -1,277 +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. - * - * @fileoverview Mocks for testing cast components. - */ - -goog.module('exoplayer.cast.test.mocks'); -goog.setTestOnly(); - -const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); - -let mockState; -let manifest; - -/** - * Initializes the state of the mocks. Needs to be called in the setUp method of - * the unit test. - */ -const setUp = function() { - mockState = { - outputMessages: {}, - listeners: {}, - loadedUri: null, - preferredTextLanguage: '', - preferredAudioLanguage: '', - configuration: null, - responseFilter: null, - isSilent: false, - customMessageListener: undefined, - mediaElementState: { - removedAttributes: [], - }, - manifestState: { - isLive: false, - windowDuration: 20, - startTime: 0, - delay: 10, - }, - getManifest: () => manifest, - setManifest: (m) => { - manifest = m; - }, - shakaError: { - severity: /** CRITICAL */ 2, - code: /** not 7000 (LOAD_INTERUPTED) */ 3, - category: /** any */ 1, - }, - simulateLoad: simulateLoadSuccess, - /** @type {function(boolean)} */ - setShakaThrowsOnLoad: (doThrow) => { - mockState.simulateLoad = doThrow ? throwShakaError : simulateLoadSuccess; - }, - simulateUnload: simulateUnloadSuccess, - /** @type {function(boolean)} */ - setShakaThrowsOnUnload: (doThrow) => { - mockState.simulateUnload = - doThrow ? throwShakaError : simulateUnloadSuccess; - }, - onSenderConnected: undefined, - onSenderDisconnected: undefined, - }; - manifest = { - periods: [{startTime: mockState.manifestState.startTime}], - presentationTimeline: { - getDuration: () => mockState.manifestState.windowDuration, - isLive: () => mockState.manifestState.isLive, - getSegmentAvailabilityStart: () => 0, - getSegmentAvailabilityEnd: () => mockState.manifestState.windowDuration, - getSeekRangeStart: () => 0, - getSeekRangeEnd: () => mockState.manifestState.windowDuration - - mockState.manifestState.delay, - }, - }; -}; - -/** - * Simulates a successful `shakaPlayer.load` call. - * - * @param {string} uri The uri to load. - */ -const simulateLoadSuccess = (uri) => { - mockState.loadedUri = uri; - notifyListeners('streaming'); -}; - -/** Simulates a successful `shakaPlayer.unload` call. */ -const simulateUnloadSuccess = () => { - mockState.loadedUri = undefined; - notifyListeners('unloading'); -}; - -/** @throws {!ShakaError} Thrown in any case. */ -const throwShakaError = () => { - throw mockState.shakaError; -}; - - -/** - * Adds a fake event listener. - * - * @param {string} type The type of the listener. - * @param {function(!Object)} listener The callback listener. - */ -const addEventListener = function(type, listener) { - mockState.listeners[type] = mockState.listeners[type] || []; - mockState.listeners[type].push(listener); -}; - -/** - * Notifies the fake listeners of the given type. - * - * @param {string} type The type of the listener to notify. - */ -const notifyListeners = function(type) { - if (mockState.isSilent || !mockState.listeners[type]) { - return; - } - for (let i = 0; i < mockState.listeners[type].length; i++) { - mockState.listeners[type][i]({ - type: type - }); - } -}; - -/** - * Creates an observable for which listeners can be added. - * - * @return {!Object} An observable object. - */ -const createObservable = () => { - return { - addEventListener: (type, listener) => { - addEventListener(type, listener); - }, - }; -}; - -/** - * Creates a fake for the shaka player. - * - * @return {!shaka.Player} A shaka player mock object. - */ -const createShakaFake = () => { - const shakaFake = /** @type {!shaka.Player} */(createObservable()); - const mediaElement = createMediaElementFake(); - /** - * @return {!HTMLMediaElement} A media element. - */ - shakaFake.getMediaElement = () => mediaElement; - shakaFake.getAudioLanguages = () => []; - shakaFake.getVariantTracks = () => []; - shakaFake.configure = (configuration) => { - mockState.configuration = configuration; - return true; - }; - shakaFake.selectTextLanguage = (language) => { - mockState.preferredTextLanguage = language; - }; - shakaFake.selectAudioLanguage = (language) => { - mockState.preferredAudioLanguage = language; - }; - shakaFake.getManifest = () => manifest; - shakaFake.unload = async () => mockState.simulateUnload(); - shakaFake.load = async (uri) => mockState.simulateLoad(uri); - shakaFake.getNetworkingEngine = () => { - return /** @type {!NetworkingEngine} */ ({ - registerResponseFilter: (responseFilter) => { - mockState.responseFilter = responseFilter; - }, - unregisterResponseFilter: (responseFilter) => { - if (mockState.responseFilter !== responseFilter) { - throw new Error('unregistering invalid response filter'); - } else { - mockState.responseFilter = null; - } - }, - }); - }; - return shakaFake; -}; - -/** - * Creates a fake for a media element. - * - * @return {!HTMLMediaElement} A media element fake. - */ -const createMediaElementFake = () => { - const mediaElementFake = /** @type {!HTMLMediaElement} */(createObservable()); - mediaElementFake.load = () => { - // Do nothing. - }; - mediaElementFake.play = () => { - mediaElementFake.paused = false; - notifyListeners('playing'); - return Promise.resolve(); - }; - mediaElementFake.pause = () => { - mediaElementFake.paused = true; - notifyListeners('pause'); - }; - mediaElementFake.seekable = /** @type {!TimeRanges} */({ - length: 1, - start: (index) => mockState.manifestState.startTime, - end: (index) => mockState.manifestState.windowDuration, - }); - mediaElementFake.removeAttribute = (name) => { - mockState.mediaElementState.removedAttributes.push(name); - if (name === 'src') { - mockState.loadedUri = null; - } - }; - mediaElementFake.hasAttribute = (name) => { - return name === 'src' && !!mockState.loadedUri; - }; - mediaElementFake.buffered = /** @type {!TimeRanges} */ ({ - length: 0, - start: (index) => null, - end: (index) => null, - }); - mediaElementFake.paused = true; - return mediaElementFake; -}; - -/** - * Creates a cast receiver manager fake. - * - * @return {!Object} A cast receiver manager fake. - */ -const createCastReceiverContextFake = () => { - return { - addCustomMessageListener: (namespace, listener) => { - mockState.customMessageListener = listener; - }, - sendCustomMessage: (namespace, senderId, message) => { - mockState.outputMessages[senderId] = - mockState.outputMessages[senderId] || []; - mockState.outputMessages[senderId].push(message); - }, - addEventListener: (eventName, listener) => { - switch (eventName) { - case 'sender_connected': - mockState.onSenderConnected = listener; - break; - case 'sender_disconnected': - mockState.onSenderDisconnected = listener; - break; - } - }, - getSenders: () => [{id: 'sender0'}], - start: () => {}, - }; -}; - -/** - * Returns the state of the mocks. - * - * @return {?Object} - */ -const state = () => mockState; - -exports.createCastReceiverContextFake = createCastReceiverContextFake; -exports.createShakaFake = createShakaFake; -exports.notifyListeners = notifyListeners; -exports.setUp = setUp; -exports.state = state; diff --git a/cast_receiver_app/test/playback_info_view_test.js b/cast_receiver_app/test/playback_info_view_test.js deleted file mode 100644 index 87cefe1884..0000000000 --- a/cast_receiver_app/test/playback_info_view_test.js +++ /dev/null @@ -1,242 +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. - * - * @fileoverview Unit tests for the playback info view. - */ - -goog.module('exoplayer.cast.test.PlaybackInfoView'); -goog.setTestOnly(); - -const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView'); -const Player = goog.require('exoplayer.cast.Player'); -const testSuite = goog.require('goog.testing.testSuite'); - -/** The state of the player mock */ -let mockState; - -/** - * Initializes the state of the mock. Needs to be called in the setUp method of - * the unit test. - */ -const setUpMockState = function() { - mockState = { - playWhenReady: false, - currentPositionMs: 1000, - durationMs: 10 * 1000, - playbackState: 'READY', - discontinuityReason: undefined, - listeners: [], - currentMediaItem: { - mimeType: 'video/*', - }, - }; -}; - -/** Notifies registered listeners with the current player state. */ -const notifyListeners = function() { - if (!mockState) { - console.warn( - 'mock state not initialized. Did you call setUp ' + - 'when setting up the test case?'); - } - mockState.listeners.forEach((listener) => { - listener({ - playWhenReady: mockState.playWhenReady, - playbackState: mockState.playbackState, - playbackPosition: { - currentPositionMs: mockState.currentPositionMs, - discontinuityReason: mockState.discontinuityReason, - }, - }); - }); -}; - -/** - * Creates a sufficient mock of the Player. - * - * @return {!Player} - */ -const createPlayerMock = function() { - return /** @type {!Player} */ ({ - addPlayerListener: (listener) => { - mockState.listeners.push(listener); - }, - getPlayWhenReady: () => mockState.playWhenReady, - getPlaybackState: () => mockState.playbackState, - getCurrentPositionMs: () => mockState.currentPositionMs, - getDurationMs: () => mockState.durationMs, - getCurrentMediaItem: () => mockState.currentMediaItem, - }); -}; - -/** Inserts the DOM structure the playback info view needs. */ -const insertComponentDom = function() { - const container = appendChild(document.body, 'div', 'container-id'); - appendChild(container, 'div', 'exo_elapsed_time'); - appendChild(container, 'div', 'exo_elapsed_time_label'); - appendChild(container, 'div', 'exo_duration_label'); -}; - -/** - * Creates and appends a child to the parent element. - * - * @param {!Element} parent The parent element. - * @param {string} tagName The tag name of the child element. - * @param {string} id The id of the child element. - * @return {!Element} The appended child element. - */ -const appendChild = function(parent, tagName, id) { - const child = document.createElement(tagName); - child.id = id; - parent.appendChild(child); - return child; -}; - -/** Removes the inserted elements from the DOM again. */ -const removeComponentDom = function() { - const container = document.getElementById('container-id'); - if (container) { - container.parentNode.removeChild(container); - } -}; - -let playbackInfoView; - -testSuite({ - setUp() { - insertComponentDom(); - setUpMockState(); - playbackInfoView = new PlaybackInfoView( - createPlayerMock(), /** containerId= */ 'container-id'); - playbackInfoView.setShowTimeoutMs(1); - }, - - tearDown() { - removeComponentDom(); - }, - - /** Tests setting the show timeout. */ - testSetShowTimeout() { - assertEquals(1, playbackInfoView.showTimeoutMs_); - playbackInfoView.setShowTimeoutMs(10); - assertEquals(10, playbackInfoView.showTimeoutMs_); - }, - - /** Tests rendering the duration to the DOM. */ - testRenderDuration() { - const el = document.getElementById('exo_duration_label'); - assertEquals('00:10', el.firstChild.firstChild.nodeValue); - mockState.durationMs = 35 * 1000; - notifyListeners(); - assertEquals('00:35', el.firstChild.firstChild.nodeValue); - - mockState.durationMs = - (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); - notifyListeners(); - assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); - - mockState.durationMs = -1000; - notifyListeners(); - assertNull(el.nodeValue); - }, - - /** Tests rendering the playback position to the DOM. */ - testRenderPlaybackPosition() { - const el = document.getElementById('exo_elapsed_time_label'); - assertEquals('00:01', el.firstChild.firstChild.nodeValue); - mockState.currentPositionMs = 2000; - notifyListeners(); - assertEquals('00:02', el.firstChild.firstChild.nodeValue); - - mockState.currentPositionMs = - (12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000); - notifyListeners(); - assertEquals('12:20:13', el.firstChild.firstChild.nodeValue); - - mockState.currentPositionMs = -1000; - notifyListeners(); - assertNull(el.nodeValue); - - mockState.currentPositionMs = 0; - notifyListeners(); - assertEquals('00:00', el.firstChild.firstChild.nodeValue); - }, - - /** Tests rendering the timebar width reflects position and duration. */ - testRenderTimebar() { - const el = document.getElementById('exo_elapsed_time'); - assertEquals('10%', el.style.width); - - mockState.currentPositionMs = 0; - notifyListeners(); - assertEquals('0px', el.style.width); - - mockState.currentPositionMs = 5 * 1000; - notifyListeners(); - assertEquals('50%', el.style.width); - - mockState.currentPositionMs = mockState.durationMs * 2; - notifyListeners(); - assertEquals('100%', el.style.width); - - mockState.currentPositionMs = -1; - notifyListeners(); - assertEquals('0px', el.style.width); - }, - - /** Tests whether the update timeout is set and removed. */ - testUpdateTimeout_setAndRemoved() { - assertFalse(playbackInfoView.updateTimeout_.isOngoing()); - - mockState.playWhenReady = true; - notifyListeners(); - assertTrue(playbackInfoView.updateTimeout_.isOngoing()); - - mockState.playWhenReady = false; - notifyListeners(); - assertFalse(playbackInfoView.updateTimeout_.isOngoing()); - }, - - /** Tests whether the show timeout is set when playback starts. */ - testHideTimeout_setAndRemoved() { - assertFalse(playbackInfoView.hideTimeout_.isOngoing()); - - mockState.playWhenReady = true; - notifyListeners(); - assertNotUndefined(playbackInfoView.hideTimeout_); - assertTrue(playbackInfoView.hideTimeout_.isOngoing()); - - mockState.playWhenReady = false; - notifyListeners(); - assertFalse(playbackInfoView.hideTimeout_.isOngoing()); - }, - - /** Test whether the view switches to always on for audio media. */ - testAlwaysOnForAudio() { - playbackInfoView.setShowTimeoutMs(50); - assertEquals(50, playbackInfoView.showTimeoutMs_); - // The player transitions from video to audio stream. - mockState.discontinuityReason = 'PERIOD_TRANSITION'; - mockState.currentMediaItem.mimeType = 'audio/*'; - notifyListeners(); - assertEquals(0, playbackInfoView.showTimeoutMs_); - - mockState.discontinuityReason = 'PERIOD_TRANSITION'; - mockState.currentMediaItem.mimeType = 'video/*'; - notifyListeners(); - assertEquals(50, playbackInfoView.showTimeoutMs_); - }, - -}); diff --git a/cast_receiver_app/test/player_test.js b/cast_receiver_app/test/player_test.js deleted file mode 100644 index 96dfbf8614..0000000000 --- a/cast_receiver_app/test/player_test.js +++ /dev/null @@ -1,470 +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. - * - * @fileoverview Unit tests for playback methods. - */ - -goog.module('exoplayer.cast.test'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const Player = goog.require('exoplayer.cast.Player'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let player; -let shakaFake; - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - }, - - /** Tests the player initialisation */ - testPlayerInitialisation() { - mocks.state().isSilent = true; - const states = []; - let stateCounter = 0; - let currentState; - player.addPlayerListener((playerState) => { - states.push(playerState); - }); - - // Dump the initial state manually. - player.invalidate(); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(0, currentState.mediaQueue.length); - assertEquals(0, currentState.windowIndex); - assertNull(currentState.playbackPosition); - - // Seek with uuid to prepare with later - const uuid = 'uuid1'; - player.seekToUuid(uuid, 30 * 1000); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(30 * 1000, player.getCurrentPositionMs()); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(-1, player.windowIndex_); - assertEquals(1, currentState.playbackPosition.periodId); - assertEquals(uuid, currentState.playbackPosition.uuid); - assertEquals(uuid, player.uuidToPrepare_); - - // Add a DASH media item. - player.addQueueItems(0, util.queue.slice(0, 2)); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals('IDLE', currentState.playbackState); - assertNotNull(currentState.playbackPosition); - util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); - - // Prepare. - player.prepare(); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(2, currentState.mediaQueue.length); - assertEquals('BUFFERING', currentState.playbackState); - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid]); - assertNull(player.uuidToPrepare_); - - // The video element starts waiting. - mocks.state().isSilent = false; - mocks.notifyListeners('waiting'); - // Nothing happens, masked buffering state after preparing. - assertEquals(stateCounter, states.length); - - // The manifest arrives. - mocks.notifyListeners('streaming'); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(2, currentState.mediaQueue.length); - assertEquals('BUFFERING', currentState.playbackState); - assertEquals(uuid, currentState.playbackPosition.uuid); - assertEquals(0, currentState.playbackPosition.periodId); - assertEquals(30 * 1000, currentState.playbackPosition.positionMs); - // The dummy media item info has been replaced by the real one. - assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); - assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); - assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); - - // Tracks have initially changed. - mocks.notifyListeners('trackschanged'); - // Nothing happens because the media item info remains the same. - assertEquals(stateCounter, states.length); - - // The video element reports the first frame rendered. - mocks.notifyListeners('loadeddata'); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - assertEquals(2, currentState.mediaQueue.length); - assertEquals('READY', currentState.playbackState); - assertEquals(uuid, currentState.playbackPosition.uuid); - assertEquals(0, currentState.playbackPosition.periodId); - assertEquals(30 * 1000, currentState.playbackPosition.positionMs); - - // Playback starts. - mocks.notifyListeners('playing'); - // Nothing happens; we are ready already. - assertEquals(stateCounter, states.length); - - // Add another queue item. - player.addQueueItems(1, util.queue.slice(3, 4)); - stateCounter++; - assertEquals(stateCounter, states.length); - mocks.state().isSilent = true; - // Seek to the next queue item. - player.seekToWindow(1, 0); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - const uuid1 = currentState.mediaQueue[1].uuid; - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid1]); - util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue); - - // The video element starts waiting. - mocks.state().isSilent = false; - mocks.notifyListeners('waiting'); - // Nothing happens, masked buffering state after preparing. - assertEquals(stateCounter, states.length); - - // The manifest arrives. - mocks.notifyListeners('streaming'); - stateCounter++; - assertEquals(stateCounter, states.length); - currentState = states[stateCounter - 1]; - // The dummy media item info has been replaced by the real one. - assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs); - assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs); - assertTrue(currentState.mediaItemsInfo[uuid].isSeekable); - assertFalse(currentState.mediaItemsInfo[uuid].isDynamic); - }, - - /** Tests next and previous window when not yet prepared. */ - testNextPreviousWindow_notPrepared() { - assertEquals(-1, player.getNextWindowIndex()); - assertEquals(-1, player.getPreviousWindowIndex()); - player.addQueueItems(0, util.queue.slice(0, 2)); - assertEquals(-1, player.getNextWindowIndex()); - assertEquals(-1, player.getPreviousWindowIndex()); - }, - - /** Tests setting play when ready. */ - testPlayWhenReady() { - player.addQueueItems(0, util.queue.slice(0, 3)); - let playWhenReady = false; - player.addPlayerListener((state) => { - playWhenReady = state.playWhenReady; - }); - - assertEquals(false, player.getPlayWhenReady()); - assertEquals(false, playWhenReady); - - player.setPlayWhenReady(true); - assertEquals(true, player.getPlayWhenReady()); - assertEquals(true, playWhenReady); - - player.setPlayWhenReady(false); - assertEquals(false, player.getPlayWhenReady()); - assertEquals(false, playWhenReady); - }, - - /** Tests seeking to another position in the actual window. */ - async testSeek_inWindow() { - player.addQueueItems(0, util.queue.slice(0, 3)); - await player.seekToWindow(0, 1000); - - assertEquals(1, shakaFake.getMediaElement().currentTime); - assertEquals(1000, player.getCurrentPositionMs()); - assertEquals(0, player.getCurrentWindowIndex()); - }, - - /** Tests seeking to another window. */ - async testSeek_nextWindow() { - player.addQueueItems(0, util.queue.slice(0, 3)); - await player.prepare(); - assertEquals(util.queue[0].media.uri, shakaFake.getMediaElement().src); - assertEquals(-1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.seekToWindow(1, 2000); - assertEquals(0, player.getPreviousWindowIndex()); - assertEquals(2, player.getNextWindowIndex()); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(util.queue[1].media.uri, mocks.state().loadedUri); - }, - - /** Tests the repeat mode 'none' */ - testRepeatMode_none() { - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(Player.RepeatMode.OFF, player.getRepeatMode()); - assertEquals(-1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.seekToWindow(2, 0); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(-1, player.getNextWindowIndex()); - }, - - /** Tests the repeat mode 'all'. */ - testRepeatMode_all() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((state) => { - repeatMode = state.repeatMode; - }); - player.setRepeatMode(Player.RepeatMode.ALL); - assertEquals(Player.RepeatMode.ALL, repeatMode); - - player.seekToWindow(0,0); - assertEquals(2, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.seekToWindow(2, 0); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(0, player.getNextWindowIndex()); - }, - - /** - * Tests navigation within the queue when repeat mode and shuffle mode is on. - */ - testRepeatMode_all_inShuffleMode() { - const initialOrder = [2, 1, 0]; - let shuffleOrder; - let windowIndex; - player.addQueueItems(0, util.queue.slice(0, 3), initialOrder); - player.prepare(); - player.addPlayerListener((state) => { - shuffleOrder = state.shuffleOrder; - windowIndex = state.windowIndex; - }); - player.setRepeatMode(Player.RepeatMode.ALL); - player.setShuffleModeEnabled(true); - assertEquals(windowIndex, player.shuffleOrder_[player.shuffleIndex_]); - assertArrayEquals(initialOrder, shuffleOrder); - - player.seekToWindow(shuffleOrder[2], 0); - assertEquals(shuffleOrder[2], windowIndex); - assertEquals(shuffleOrder[0], player.getNextWindowIndex()); - assertEquals(shuffleOrder[1], player.getPreviousWindowIndex()); - - player.seekToWindow(shuffleOrder[0], 0); - assertEquals(shuffleOrder[0], windowIndex); - }, - - /** Tests the repeat mode 'one' */ - testRepeatMode_one() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((state) => { - repeatMode = state.repeatMode; - }); - player.setRepeatMode(Player.RepeatMode.ONE); - assertEquals(Player.RepeatMode.ONE, repeatMode); - assertEquals(0, player.getPreviousWindowIndex()); - assertEquals(0, player.getNextWindowIndex()); - - player.seekToWindow(1, 0); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - - player.setShuffleModeEnabled(true); - assertEquals(1, player.getPreviousWindowIndex()); - assertEquals(1, player.getNextWindowIndex()); - }, - - /** Tests building a media item info from the manifest. */ - testBuildMediaItemInfo_fromManifest() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - player.seekToWindow(1, 0); - player.prepare(); - assertUndefined(mediaItemInfos['uuid0']); - const mediaItemInfo = mediaItemInfos['uuid1']; - assertNotUndefined(mediaItemInfo); - assertFalse(mediaItemInfo.isDynamic); - assertTrue(mediaItemInfo.isSeekable); - assertEquals(0, mediaItemInfo.defaultStartPositionUs); - assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); - assertEquals(1, mediaItemInfo.periods.length); - assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); - }, - - /** Tests building a media item info with multiple periods. */ - testBuildMediaItemInfo_fromManifest_multiPeriod() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - // Setting manifest properties to emulate a multiperiod stream manifest. - mocks.state().getManifest().periods.push({startTime: 20}); - mocks.state().manifestState.windowDuration = 50; - player.seekToWindow(1, 0); - player.prepare(); - - const mediaItemInfo = mediaItemInfos['uuid1']; - assertNotUndefined(mediaItemInfo); - assertFalse(mediaItemInfo.isDynamic); - assertTrue(mediaItemInfo.isSeekable); - assertEquals(0, mediaItemInfo.defaultStartPositionUs); - assertEquals(50 * 1000 * 1000, mediaItemInfo.windowDurationUs); - assertEquals(2, mediaItemInfo.periods.length); - assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); - assertEquals(30 * 1000 * 1000, mediaItemInfo.periods[1].durationUs); - }, - - /** Tests building a media item info from a live manifest. */ - testBuildMediaItemInfo_fromManifest_live() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - // Setting manifest properties to emulate a live stream manifest. - mocks.state().manifestState.isLive = true; - mocks.state().manifestState.windowDuration = 30; - mocks.state().manifestState.delay = 10; - mocks.state().getManifest().periods.push({startTime: 20}); - player.seekToWindow(1, 0); - player.prepare(); - - const mediaItemInfo = mediaItemInfos['uuid1']; - assertNotUndefined(mediaItemInfo); - assertTrue(mediaItemInfo.isDynamic); - assertTrue(mediaItemInfo.isSeekable); - assertEquals(20 * 1000 * 1000, mediaItemInfo.defaultStartPositionUs); - assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs); - assertEquals(2, mediaItemInfo.periods.length); - assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs); - assertEquals(Infinity, mediaItemInfo.periods[1].durationUs); - }, - - /** Tests whether the shaka request filter is set for life streams. */ - testRequestFilterIsSetAndRemovedForLive() { - player.addQueueItems(0, util.queue.slice(0, 3)); - - // Set manifest properties to emulate a live stream manifest. - mocks.state().manifestState.isLive = true; - mocks.state().manifestState.windowDuration = 30; - mocks.state().manifestState.delay = 10; - mocks.state().getManifest().periods.push({startTime: 20}); - - assertNull(mocks.state().responseFilter); - assertFalse(player.isManifestFilterRegistered_); - player.seekToWindow(1, 0); - player.prepare(); - assertNotNull(mocks.state().responseFilter); - assertTrue(player.isManifestFilterRegistered_); - - // Set manifest properties to emulate a non-live stream */ - mocks.state().manifestState.isLive = false; - mocks.state().manifestState.windowDuration = 20; - mocks.state().manifestState.delay = 0; - mocks.state().getManifest().periods.push({startTime: 20}); - - player.seekToWindow(0, 0); - assertNull(mocks.state().responseFilter); - assertFalse(player.isManifestFilterRegistered_); - }, - - /** Tests whether the media info is removed when queue item is removed. */ - testRemoveMediaItemInfo() { - let mediaItemInfos = null; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - mediaItemInfos = state.mediaItemsInfo; - }); - player.seekToWindow(1, 0); - player.prepare(); - assertNotUndefined(mediaItemInfos['uuid1']); - player.removeQueueItems(['uuid1']); - assertUndefined(mediaItemInfos['uuid1']); - }, - - /** Tests shuffling. */ - testSetShuffeModeEnabled() { - let shuffleModeEnabled = false; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.addPlayerListener((state) => { - shuffleModeEnabled = state.shuffleModeEnabled; - }); - player.setShuffleModeEnabled(true); - assertTrue(shuffleModeEnabled); - - player.setShuffleModeEnabled(false); - assertFalse(shuffleModeEnabled); - }, - - /** Tests setting a new playback order. */ - async testSetShuffleOrder() { - const defaultOrder = [0, 1, 2]; - let shuffleOrder; - player.addPlayerListener((state) => { - shuffleOrder = state.shuffleOrder; - }); - await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); - assertArrayEquals(defaultOrder, shuffleOrder); - - player.setShuffleOrder_([2, 1, 0]); - assertArrayEquals([2, 1, 0], player.shuffleOrder_); - }, - - /** Tests setting a new playback order with incorrect length. */ - async testSetShuffleOrder_incorrectLength() { - const defaultOrder = [0, 1, 2]; - let shuffleOrder; - player.addPlayerListener((state) => { - shuffleOrder = state.shuffleOrder; - }); - await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder); - assertArrayEquals(defaultOrder, shuffleOrder); - - shuffleOrder = undefined; - player.setShuffleOrder_([2, 1]); - assertUndefined(shuffleOrder); - }, - - /** Tests falling into ENDED when prepared with empty queue. */ - testPrepare_withEmptyQueue() { - player.seekToUuid('uuid1000', 1000); - assertEquals('uuid1000', player.uuidToPrepare_); - player.prepare(); - assertEquals('ENDED', player.getPlaybackState()); - assertNull(player.uuidToPrepare_); - player.seekToUuid('uuid1000', 1000); - assertNull(player.uuidToPrepare_); - }, -}); diff --git a/cast_receiver_app/test/queue_test.js b/cast_receiver_app/test/queue_test.js deleted file mode 100644 index b46361fb2e..0000000000 --- a/cast_receiver_app/test/queue_test.js +++ /dev/null @@ -1,166 +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. - * - * @fileoverview Unit tests for queue manipulations. - */ - -goog.module('exoplayer.cast.test.queue'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const Player = goog.require('exoplayer.cast.Player'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let player; - -testSuite({ - setUp() { - mocks.setUp(); - player = new Player(mocks.createShakaFake(), new ConfigurationFactory()); - }, - - /** Tests adding queue items. */ - testAddQueueItem() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - assertEquals(0, queue.length); - player.addQueueItems(0, util.queue.slice(0, 3)); - assertEquals(util.queue[0].media.uri, queue[0].media.uri); - assertEquals(util.queue[1].media.uri, queue[1].media.uri); - assertEquals(util.queue[2].media.uri, queue[2].media.uri); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests that duplicate queue items are ignored. */ - testAddDuplicateQueueItem() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - assertEquals(0, queue.length); - // Insert three items. - player.addQueueItems(0, util.queue.slice(0, 3)); - // Insert two of which the first is a duplicate. - player.addQueueItems(1, util.queue.slice(2, 4)); - assertEquals(4, queue.length); - assertArrayEquals( - ['uuid0', 'uuid3', 'uuid1', 'uuid2'], queue.slice().map((i) => i.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving queue items. */ - testMoveQueueItem() { - const shuffleOrder = [0, 2, 1]; - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.moveQueueItem('uuid0', 1, shuffleOrder); - assertEquals(util.queue[1].media.uri, queue[0].media.uri); - assertEquals(util.queue[0].media.uri, queue[1].media.uri); - assertEquals(util.queue[2].media.uri, queue[2].media.uri); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - - queue = undefined; - // invalid to index - player.moveQueueItem('uuid0', 11, [0, 1, 2]); - assertTrue(typeof queue === 'undefined'); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - // negative to index - player.moveQueueItem('uuid0', -11, shuffleOrder); - assertTrue(typeof queue === 'undefined'); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - // unknown uuid - player.moveQueueItem('unknown', 1, shuffleOrder); - assertTrue(typeof queue === 'undefined'); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - }, - - /** Tests removing queue items. */ - testRemoveQueueItems() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.prepare(); - player.seekToWindow(1, 0); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Remove the first item. - player.removeQueueItems(['uuid0']); - assertEquals(2, queue.length); - assertEquals(util.queue[1].media.uri, queue[0].media.uri); - assertEquals(util.queue[2].media.uri, queue[1].media.uri); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([1,0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Calling stop without reseting preserves the queue. - player.stop(false); - assertEquals('uuid1', player.uuidToPrepare_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Remove the item at the end of the queue. - player.removeQueueItems(['uuid2']); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - - // Remove the last remaining item in the queue. - player.removeQueueItems(['uuid1']); - assertEquals(0, queue.length); - assertEquals('IDLE', player.getPlaybackState()); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([], player.shuffleOrder_); - assertNull(player.uuidToPrepare_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_); - }, - - /** Tests removing multiple unordered queue items at once. */ - testRemoveQueueItems_multiple() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 6), []); - player.prepare(); - - assertEquals(6, queue.length); - player.removeQueueItems(['uuid1', 'uuid5', 'uuid3']); - assertArrayEquals(['uuid0', 'uuid2', 'uuid4'], queue.map((i) => i.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests whether stopping with reset=true resets queue and uuidToIndexMap */ - testStop_resetTrue() { - let queue = []; - player.addPlayerListener((state) => { - queue = state.mediaQueue; - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.prepare(); - player.stop(true); - assertEquals(0, player.queue_.length); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, -}); diff --git a/cast_receiver_app/test/receiver_test.js b/cast_receiver_app/test/receiver_test.js deleted file mode 100644 index 303a1caf64..0000000000 --- a/cast_receiver_app/test/receiver_test.js +++ /dev/null @@ -1,1027 +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. - * - * @fileoverview Unit tests for receiver. - */ - -goog.module('exoplayer.cast.test.receiver'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const Player = goog.require('exoplayer.cast.Player'); -const Receiver = goog.require('exoplayer.cast.Receiver'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -/** @type {?Player|undefined} */ -let player; -/** @type {!Array} */ -let queue = []; -let shakaFake; -let castContextMock; - -/** - * Sends a message to the receiver under test. - * - * @param {!Object} message The message to send as json. - */ -const sendMessage = function(message) { - mocks.state().customMessageListener({ - data: message, - senderId: 'sender0', - }); -}; - -/** - * Creates a valid media item with the suffix appended to each field. - * - * @param {string} suffix The suffix to append to the fields value. - * @return {!Object} The media item. - */ -const createMediaItem = function(suffix) { - return { - uuid: 'uuid' + suffix, - media: {uri: 'uri' + suffix}, - mimeType: 'application/dash+xml', - }; -}; - -let messageSequence = 0; - -/** - * Creates a message in the format sent bey the sender app. - * - * @param {string} method The name of the method. - * @param {?Object} args The arguments. - * @return {!Object} The message. - */ -const createMessage = function (method, args) { - return { - method: method, - args: args, - sequenceNumber: ++messageSequence, - }; -}; - -/** - * Asserts the `playerState` is in the same state as just after creation of the - * player. - * - * @param {!PlayerState} playerState The player state to assert. - * @param {string} playbackState The expected playback state. - */ -const assertInitialState = function(playerState, playbackState) { - assertEquals(playbackState, playerState.playbackState); - // Assert the state is in initial state. - assertArrayEquals([], queue); - assertEquals(0, playerState.windowCount); - assertEquals(0, playerState.windowIndex); - assertUndefined(playerState.playbackError); - assertNull(playerState.playbackPosition); - // Assert player properties. - assertEquals(0, player.getDurationMs()); - assertArrayEquals([], Object.entries(player.mediaItemInfoMap_)); - assertEquals(0, player.windowPeriodIndex_); - assertEquals(999, player.playbackType_); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); -}; - - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - castContextMock = mocks.createCastReceiverContextFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - const messageDispatcher = new MessageDispatcher( - 'urn:x-cast:com.google.exoplayer.cast', castContextMock); - new Receiver(player, castContextMock, messageDispatcher); - }, - - tearDown() { - queue = []; - }, - - /** Tests whether a status was sent to the sender on connect. */ - testNotifyClientConnected() { - assertUndefined(mocks.state().outputMessages['sender0']); - - sendMessage(createMessage('player.onClientConnected', {})); - const message = mocks.state().outputMessages['sender0'][0]; - assertEquals(messageSequence, message.sequenceNumber); - }, - - /** - * Tests whether a custom message listener has been registered after - * construction. - */ - testCustomMessageListener() { - assertTrue(goog.isFunction(mocks.state().customMessageListener)); - }, - - /** Tests set playWhenReady. */ - testSetPlayWhenReady() { - let playWhenReady; - player.addPlayerListener((playerState) => { - playWhenReady = playerState.playWhenReady; - }); - - sendMessage(createMessage( - 'player.setPlayWhenReady', - { playWhenReady: true } - )); - assertTrue(playWhenReady); - sendMessage(createMessage( - 'player.setPlayWhenReady', - { playWhenReady: false } - )); - assertFalse(playWhenReady); - }, - - /** Tests setting repeat modes. */ - testSetRepeatMode() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((playerState) => { - repeatMode = playerState.repeatMode; - }); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.ONE } - )); - assertEquals(Player.RepeatMode.ONE, repeatMode); - assertEquals(0, player.getNextWindowIndex()); - assertEquals(0, player.getPreviousWindowIndex()); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.ALL } - )); - assertEquals(Player.RepeatMode.ALL, repeatMode); - assertEquals(1, player.getNextWindowIndex()); - assertEquals(2, player.getPreviousWindowIndex()); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.OFF } - )); - assertEquals(Player.RepeatMode.OFF, repeatMode); - assertEquals(1, player.getNextWindowIndex()); - assertTrue(player.getPreviousWindowIndex() < 0); - }, - - /** Tests setting an invalid repeat mode value. */ - testSetRepeatMode_invalid_noStateChange() { - let repeatMode; - player.addPlayerListener((playerState) => { - repeatMode = playerState.repeatMode; - }); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: "UNKNOWN" } - )); - assertEquals(Player.RepeatMode.OFF, player.repeatMode_); - assertUndefined(repeatMode); - player.invalidate(); - assertEquals(Player.RepeatMode.OFF, repeatMode); - }, - - /** Tests enabling and disabling shuffle mode. */ - testSetShuffleModeEnabled() { - const enableMessage = createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: true, - }); - const disableMessage = createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: false, - }); - let shuffleModeEnabled; - player.addPlayerListener((state) => { - shuffleModeEnabled = state.shuffleModeEnabled; - }); - assertFalse(player.shuffleModeEnabled_); - sendMessage(enableMessage); - assertTrue(shuffleModeEnabled); - sendMessage(disableMessage); - assertFalse(shuffleModeEnabled); - }, - - /** Tests adding a single media item to the queue. */ - testAddMediaItem_single() { - const suffix = '0'; - const jsonMessage = createMessage('player.addItems', { - index: 0, - items: [ - createMediaItem(suffix), - ], - shuffleOrder: [0], - }); - - sendMessage(jsonMessage); - assertEquals(1, queue.length); - assertEquals('uuid0', queue[0].uuid); - assertEquals('uri0', queue[0].media.uri); - assertArrayEquals([0], player.shuffleOrder_); - }, - - /** Tests adding multiple media items to the queue. */ - testAddMediaItem_multiple() { - const shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.addItems', { - index: 0, - items: [ - createMediaItem('0'), - createMediaItem('1'), - createMediaItem('2'), - ], - shuffleOrder: shuffleOrder, - }); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - }, - - /** Tests adding a media item to end of the queue by omitting the index. */ - testAddMediaItem_noindex_addstoend() { - const shuffleOrder = [1, 3, 2, 0]; - const jsonMessage = createMessage('player.addItems', { - items: [createMediaItem('99')], - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - let queue = []; - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid99', queue[3].uuid); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - }, - - /** Tests adding items with a shuffle order of invalid length. */ - testAddMediaItems_invalidShuffleOrderLength() { - const shuffleOrder = [1, 3, 2]; - const jsonMessage = createMessage('player.addItems', { - items: [createMediaItem('99')], - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - let queue = []; - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid99', queue[3].uuid); - assertEquals(4, player.shuffleOrder_.length); - }, - - /** Tests inserting a media item to the queue. */ - testAddMediaItem_insert() { - const index = 1; - const shuffleOrder = [1, 0, 3, 2, 4]; - const firstInsertionMessage = createMessage('player.addItems', { - index, - items: [ - createMediaItem('99'), - createMediaItem('100'), - ], - shuffleOrder, - }); - const prepareMessage = createMessage('player.prepare', {}); - const secondInsertionMessage = createMessage('player.addItems', { - index, - items: [ - createMediaItem('199'), - createMediaItem('1100'), - ], - shuffleOrder, - }); - // fill with three items - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToUuid('uuid99', 0); - - sendMessage(firstInsertionMessage); - // The window index does not change when IDLE. - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(5, queue.length); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - - // Prepare sets the index by the uuid to which we seeked. - sendMessage(prepareMessage); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - // Add two items at the current window index. - sendMessage(secondInsertionMessage); - // Current window index is adjusted. - assertEquals(3, player.getCurrentWindowIndex()); - assertEquals(7, queue.length); - assertEquals('uuid199', queue[index].uuid); - assertEquals(7, player.shuffleOrder_.length); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests adding a media item with an index larger than the queue size. */ - testAddMediaItem_indexLargerThanQueueSize_addsToEnd() { - const index = 4; - const jsonMessage = createMessage('player.addItems', { - index: index, - items: [ - createMediaItem('99'), - createMediaItem('100'), - ], - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid99', 'uuid100'], - queue.map((x) => x.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing an item from the queue. */ - testRemoveMediaItem() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); - assertArrayEquals([0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the currently playing item from the queue. */ - async testRemoveMediaItem_currentItem() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(1, 0); - player.prepare(); - - await sendMessage(jsonMessage); - assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(util.queue[2].media.uri, shakaFake.getMediaElement().src); - assertArrayEquals([0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing items which affect the current window index. */ - async testRemoveMediaItem_affectsWindowIndex() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - const currentUri = util.queue[4].media.uri; - player.addQueueItems(0, util.queue.slice(0, 6), [3, 2, 1, 4, 0, 5]); - player.prepare(); - await player.seekToWindow(4, 2000); - assertEquals(currentUri, shakaFake.getMediaElement().src); - - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid4', queue[player.getCurrentWindowIndex()].uuid); - assertEquals(2, player.getCurrentWindowIndex()); - assertEquals(currentUri, shakaFake.getMediaElement().src); - assertArrayEquals([1, 0, 2, 3], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the last item of the queue. */ - testRemoveMediaItem_firstItem_windowIndexIsCorrect() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(1, 0); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([1, 0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the last item of the queue. */ - testRemoveMediaItem_lastItem_windowIndexIsCorrect() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid2']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(2, 0); - player.prepare(); - - mocks.state().isSilent = true; - const states = []; - player.addPlayerListener((playerState) => { - states.push(playerState); - }); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - assertArrayEquals([0, 1], player.shuffleOrder_); - assertEquals(1, states.length); - assertEquals(Player.PlaybackState.BUFFERING, states[0].playbackState); - assertEquals( - Player.DiscontinuityReason.PERIOD_TRANSITION, - states[0].playbackPosition.discontinuityReason); - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, states[0].mediaItemsInfo['uuid1']); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing items all items. */ - testRemoveMediaItem_removeAll() { - const jsonMessage = createMessage('player.removeItems', - {uuids: ['uuid1', 'uuid0', 'uuid2']}); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(2, 2000); - player.prepare(); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - - sendMessage(jsonMessage); - assertInitialState(playerState, 'ENDED'); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([], player.shuffleOrder_); - assertEquals(Player.PlaybackState.ENDED, player.getPlaybackState()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, []); - }, - - /** Tests moving an item in the queue. */ - testMoveItem() { - let shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid2', - index: 0, - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving the currently playing item in the queue. */ - testMoveItem_currentWindowIndex() { - let shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid2', - index: 0, - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToUuid('uuid2', 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from before to after the currently playing item. */ - testMoveItem_decreaseCurrentWindowIndex() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid0', - index: 5, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5', 'uuid0'], - queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from after to before the currently playing item. */ - testMoveItem_increaseCurrentWindowIndex() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid5', - index: 0, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid5', 'uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4'], - queue.map((x) => x.uuid)); - assertEquals(3, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from after to the current window index. */ - testMoveItem_toCurrentWindowIndex_fromAfter() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid5', - index: 2, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid5', 'uuid2', 'uuid3', 'uuid4'], - queue.map((x) => x.uuid)); - assertEquals(3, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from before to the current window index. */ - testMoveItem_toCurrentWindowIndex_fromBefore() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid0', - index: 2, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2', 'uuid0', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests seekTo. */ - testSeekTo() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid1', - 'positionMs': 2000 - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - sendMessage(jsonMessage); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo to unknown uuid. */ - testSeekTo_unknownUuid() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'unknown', - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToWindow(1, 2000); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - - sendMessage(jsonMessage); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo without position. */ - testSeekTo_noPosition_defaultsToZero() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid1', - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - sendMessage(jsonMessage); - assertEquals(0, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo to negative position. */ - testSeekTo_negativePosition_defaultsToZero() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid2', - 'positionMs': -1, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToWindow(1, 2000); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - - sendMessage(jsonMessage); - assertEquals(0, player.getCurrentPositionMs()); - assertEquals(2, player.getCurrentWindowIndex()); - }, - - /** Tests whether validation is turned on. */ - testMediaItemValidation_isOn() { - const index = 0; - const mediaItem = createMediaItem('99'); - delete mediaItem.uuid; - const jsonMessage = createMessage('player.addItems', { - index: index, - items: [mediaItem], - shuffleOrder: [], - }); - - sendMessage(jsonMessage); - assertEquals(0, queue.length); - }, - - /** Tests whether the state is sent to sender apps on state transition. */ - testPlayerStateIsSent_withCorrectSequenceNumber() { - assertUndefined(mocks.state().outputMessages['sender0']); - const playMessage = - createMessage('player.setPlayWhenReady', {playWhenReady: true}); - sendMessage(playMessage); - - const playerState = mocks.state().outputMessages['sender0'][0]; - assertTrue(playerState.playWhenReady); - assertEquals(playMessage.sequenceNumber, playerState.sequenceNumber); - }, - - /** Tests whether a connect of a sender app sends the current player state. */ - testSenderConnection() { - const onSenderConnected = mocks.state().onSenderConnected; - assertTrue(goog.isFunction(onSenderConnected)); - onSenderConnected({senderId: 'sender0'}); - - const playerState = mocks.state().outputMessages['sender0'][0]; - assertEquals(Player.RepeatMode.OFF, playerState.repeatMode); - assertEquals('IDLE', playerState.playbackState); - assertArrayEquals([], playerState.mediaQueue); - assertEquals(-1, playerState.sequenceNumber); - }, - - /** Tests whether a disconnect of a sender notifies the message dispatcher. */ - testSenderDisconnection_callsMessageDispatcher() { - mocks.setUp(); - let notifiedSenderId; - const myPlayer = new Player(mocks.createShakaFake()); - const myManagerFake = mocks.createCastReceiverContextFake(); - new Receiver(myPlayer, myManagerFake, { - registerActionHandler() {}, - notifySenderDisconnected(senderId) { - notifiedSenderId = senderId; - }, - }); - - const onSenderDisconnected = mocks.state().onSenderDisconnected; - assertTrue(goog.isFunction(onSenderDisconnected)); - onSenderDisconnected({senderId: 'sender0'}); - assertEquals('sender0', notifiedSenderId); - }, - - /** - * Tests whether the state right after creation of the player matches - * expectations. - */ - testInitialState() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - assertEquals(0, player.getCurrentPositionMs()); - // Dump a player state to the listener. - player.invalidate(); - // Asserts the state just after creation. - assertInitialState(playerState, 'IDLE'); - }, - - /** Tests whether user properties can be changed when in IDLE state */ - testChangingUserPropertiesWhenIdle() { - mocks.state().isSilent = true; - const states = []; - let counter = 0; - player.addPlayerListener((state) => { - states.push(state); - }); - // Adding items when IDLE. - player.addQueueItems(0, util.queue.slice(0, 3)); - counter++; - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - assertArrayEquals( - ['uuid0', 'uuid1', 'uuid2'], - states[counter - 1].mediaQueue.map((i) => i.uuid)); - - // Set playWhenReady when IDLE. - assertFalse(player.getPlayWhenReady()); - player.setPlayWhenReady(true); - counter++; - assertTrue(player.getPlayWhenReady()); - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - - // Seeking when IDLE. - player.seekToUuid('uuid2', 1000); - counter++; - // Window index not set when idle. - assertEquals(2, player.getCurrentWindowIndex()); - assertEquals(1000, player.getCurrentPositionMs()); - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - // But window index is set when prepared. - player.prepare(); - assertEquals(2, player.getCurrentWindowIndex()); - }, - - /** Tests the state after calling prepare. */ - testPrepare() { - mocks.state().isSilent = true; - const states = []; - let counter = 0; - player.addPlayerListener((state) => { - states.push(state); - }); - const prepareMessage = createMessage('player.prepare', {}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(1, 1000); - counter += 2; - - // Sends prepare message. - sendMessage(prepareMessage); - counter++; - assertEquals(counter, states.length); - assertEquals('uuid1', states[counter - 1].playbackPosition.uuid); - assertEquals( - Player.PlaybackState.BUFFERING, states[counter - 1].playbackState); - - // Fakes Shaka events. - mocks.state().isSilent = false; - mocks.notifyListeners('streaming'); - mocks.notifyListeners('loadeddata'); - counter += 2; - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.READY, states[counter - 1].playbackState); - }, - - /** Tests stopping the player with `reset=true`. */ - testStop_resetTrue() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const stopMessage = createMessage('player.stop', {reset: true}); - - player.setRepeatMode(Player.RepeatMode.ALL); - player.setShuffleModeEnabled(true); - player.setPlayWhenReady(true); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - mocks.state().isSilent = false; - mocks.notifyListeners('loadeddata'); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); - assertEquals(0, playerState.windowIndex); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(1, player.playbackType_); - // Stop the player. - sendMessage(stopMessage); - // Asserts the state looks the same as just after creation. - assertInitialState(playerState, 'IDLE'); - assertNull(playerState.playbackPosition); - // Assert player properties are preserved. - assertTrue(playerState.shuffleModeEnabled); - assertTrue(playerState.playWhenReady); - assertEquals(Player.RepeatMode.ALL, playerState.repeatMode); - }, - - /** Tests stopping the player with `reset=false`. */ - testStop_resetFalse() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const stopMessage = createMessage('player.stop', {reset: false}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToUuid('uuid1', 1000); - mocks.state().isSilent = false; - mocks.notifyListeners('streaming'); - mocks.notifyListeners('trackschanged'); - mocks.notifyListeners('loadeddata'); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); - assertEquals(1, playerState.windowIndex); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(2, player.playbackType_); - // Stop the player. - sendMessage(stopMessage); - assertEquals('IDLE', playerState.playbackState); - assertUndefined(playerState.playbackError); - // Assert the timeline is preserved. - assertEquals(3, queue.length); - assertEquals(3, playerState.windowCount); - assertEquals(1, player.windowIndex_); - assertEquals(1, playerState.windowIndex); - // Assert the playback position is correct. - assertEquals(1000, playerState.playbackPosition.positionMs); - assertEquals('uuid1', playerState.playbackPosition.uuid); - assertEquals(0, playerState.playbackPosition.periodId); - assertNull(playerState.playbackPosition.discontinuityReason); - assertEquals(1000, player.getCurrentPositionMs()); - // Assert player properties are preserved. - assertEquals(20000, player.getDurationMs()); - assertEquals(2, Object.entries(player.mediaItemInfoMap_).length); - assertEquals(0, player.windowPeriodIndex_); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(1, player.windowIndex_); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(999, player.playbackType_); - assertEquals('uuid1', player.uuidToPrepare_); - }, - - /** - * Tests the state after having removed the last item in the queue. This - * resolves to the same state like calling `stop(true)` except that the state - * is ENDED and the queue is naturally empty and hence the windowIndex is - * unset. - */ - testRemoveLastQueueItem() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const removeAllItemsMessage = createMessage( - 'player.removeItems', {uuids: ['uuid0', 'uuid1', 'uuid2']}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(0, 1000); - player.prepare(); - mocks.state().isSilent = false; - mocks.notifyListeners('loadeddata'); - // Remove all items. - sendMessage(removeAllItemsMessage); - // Assert the state after removal of all items. - assertInitialState(playerState, 'ENDED'); - }, - - /** Tests whether a player state is sent when no item is added. */ - testAddItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = createMessage('player.addItems', { - index: 0, - items: [ - util.queue[0], - ], - shuffleOrder: [0], - }); - player.addQueueItems(0, [util.queue[0]], []); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether a player state is sent when no item is removed. */ - testRemoveItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = - createMessage('player.removeItems', {uuids: ['uuid00']}); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether a player state is sent when item is not moved. */ - testMoveItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = createMessage('player.moveItem', { - uuid: 'uuid00', - index: 0, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether playback actions send a state when no-op */ - testNoOpPlaybackActionsSendPlayerState() { - mocks.state().isSilent = true; - let playerStates = []; - let parsedMessage; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - - const outputMessages = mocks.state().outputMessages['sender0']; - const setupMessageCount = playerStates.length; - let totalMessageCount = setupMessageCount; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - - const firstNoOpMessage = createMessage('player.setPlayWhenReady', { - playWhenReady: false, - }); - let expectedSequenceNumber = firstNoOpMessage.sequenceNumber; - - sendMessage(firstNoOpMessage); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.setRepeatMode', { - repeatMode: 'OFF', - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: false, - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.seekTo', { - uuid: 'not_existing', - positionMs: 0, - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - }, -}); diff --git a/cast_receiver_app/test/shaka_error_handling_test.js b/cast_receiver_app/test/shaka_error_handling_test.js deleted file mode 100644 index a7dafd3176..0000000000 --- a/cast_receiver_app/test/shaka_error_handling_test.js +++ /dev/null @@ -1,84 +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. - * - * @fileoverview Unit tests for playback methods. - */ - -goog.module('exoplayer.cast.test.shaka'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const Player = goog.require('exoplayer.cast.Player'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -let player; -let shakaFake; - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - }, - - /** Tests Shaka critical error handling on load. */ - async testShakaCriticalError_onload() { - mocks.state().isSilent = true; - mocks.state().setShakaThrowsOnLoad(true); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - player.addQueueItems(0, util.queue.slice(0, 2)); - player.seekToUuid('uuid1', 2000); - player.setPlayWhenReady(true); - // Calling prepare triggers a critical Shaka error. - await player.prepare(); - // Assert player state after error. - assertEquals('IDLE', playerState.playbackState); - assertEquals(mocks.state().shakaError.category, playerState.error.category); - assertEquals(mocks.state().shakaError.code, playerState.error.code); - assertEquals( - 'loading failed for uri: http://example1.com', - playerState.error.message); - assertEquals(999, player.playbackType_); - // Assert player properties are preserved. - assertEquals(2000, player.getCurrentPositionMs()); - assertTrue(player.getPlayWhenReady()); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(1, player.windowIndex_); - }, - - /** Tests Shaka critical error handling on unload. */ - async testShakaCriticalError_onunload() { - mocks.state().isSilent = true; - mocks.state().setShakaThrowsOnUnload(true); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - player.addQueueItems(0, util.queue.slice(0, 2)); - player.setPlayWhenReady(true); - assertUndefined(player.videoElement_.src); - // Calling prepare triggers a critical Shaka error. - await player.prepare(); - // Assert player state after caught and ignored error. - await assertEquals('BUFFERING', playerState.playbackState); - assertEquals('http://example.com', player.videoElement_.src); - assertEquals(1, player.playbackType_); - }, -}); diff --git a/cast_receiver_app/test/util.js b/cast_receiver_app/test/util.js deleted file mode 100644 index 22244675b7..0000000000 --- a/cast_receiver_app/test/util.js +++ /dev/null @@ -1,87 +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. - * - * @fileoverview Description of this file. - */ - -goog.module('exoplayer.cast.test.util'); -goog.setTestOnly(); - -/** - * The queue of sample media items - * - * @type {!Array} - */ -const queue = [ - { - uuid: 'uuid0', - media: { - uri: 'http://example.com', - }, - mimeType: 'video/*', - }, - { - uuid: 'uuid1', - media: { - uri: 'http://example1.com', - }, - mimeType: 'application/dash+xml', - }, - { - uuid: 'uuid2', - media: { - uri: 'http://example2.com', - }, - mimeType: 'video/*', - }, - { - uuid: 'uuid3', - media: { - uri: 'http://example3.com', - }, - mimeType: 'application/dash+xml', - }, - { - uuid: 'uuid4', - media: { - uri: 'http://example4.com', - }, - mimeType: 'video/*', - }, - { - uuid: 'uuid5', - media: { - uri: 'http://example5.com', - }, - mimeType: 'application/dash+xml', - }, -]; - -/** - * Asserts whether the map of uuids is complete and points to the correct - * indices. - * - * @param {!Object} uuidIndexMap The uuid to index map. - * @param {!Array} queue The media item queue. - */ -const assertUuidIndexMap = (uuidIndexMap, queue) => { - assertEquals(queue.length, Object.entries(uuidIndexMap).length); - queue.forEach((mediaItem, index) => { - assertEquals(uuidIndexMap[mediaItem.uuid], index); - }); -}; - -exports.queue = queue; -exports.assertUuidIndexMap = assertUuidIndexMap; diff --git a/cast_receiver_app/test/validation_test.js b/cast_receiver_app/test/validation_test.js deleted file mode 100644 index 8e58185cfa..0000000000 --- a/cast_receiver_app/test/validation_test.js +++ /dev/null @@ -1,266 +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. - * - * @fileoverview Unit tests for queue manipulations. - */ - -goog.module('exoplayer.cast.test.validation'); -goog.setTestOnly(); - -const testSuite = goog.require('goog.testing.testSuite'); -const validation = goog.require('exoplayer.cast.validation'); - -/** - * Creates a sample drm media for validation tests. - * - * @return {!Object} A dummy media item with a drm scheme. - */ -const createDrmMedia = function() { - return { - uuid: 'string', - media: { - uri: 'string', - }, - mimeType: 'application/dash+xml', - drmSchemes: [ - { - uuid: 'string', - licenseServer: { - uri: 'string', - requestHeaders: { - 'string': 'string', - }, - }, - }, - ], - }; -}; - -testSuite({ - - /** Tests minimal valid media item. */ - testValidateMediaItem_minimal() { - const mediaItem = { - uuid: 'string', - media: { - uri: 'string', - }, - mimeType: 'application/dash+xml', - }; - assertTrue(validation.validateMediaItem(mediaItem)); - - const uuid = mediaItem.uuid; - delete mediaItem.uuid; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.uuid = uuid; - assertTrue(validation.validateMediaItem(mediaItem)); - - const mimeType = mediaItem.mimeType; - delete mediaItem.mimeType; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.mimeType = mimeType; - assertTrue(validation.validateMediaItem(mediaItem)); - - const media = mediaItem.media; - delete mediaItem.media; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.media = media; - assertTrue(validation.validateMediaItem(mediaItem)); - - const uri = mediaItem.media.uri; - delete mediaItem.media.uri; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.media.uri = uri; - assertTrue(validation.validateMediaItem(mediaItem)); - }, - - /** Tests media item drm property validation. */ - testValidateMediaItem_drmSchemes() { - const mediaItem = createDrmMedia(); - assertTrue(validation.validateMediaItem(mediaItem)); - - const uuid = mediaItem.drmSchemes[0].uuid; - delete mediaItem.drmSchemes[0].uuid; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.drmSchemes[0].uuid = uuid; - assertTrue(validation.validateMediaItem(mediaItem)); - - const licenseServer = mediaItem.drmSchemes[0].licenseServer; - delete mediaItem.drmSchemes[0].licenseServer; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.drmSchemes[0].licenseServer = licenseServer; - assertTrue(validation.validateMediaItem(mediaItem)); - - const uri = mediaItem.drmSchemes[0].licenseServer.uri; - delete mediaItem.drmSchemes[0].licenseServer.uri; - assertFalse(validation.validateMediaItem(mediaItem)); - mediaItem.drmSchemes[0].licenseServer.uri = uri; - assertTrue(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validation of startPositionUs and endPositionUs. */ - testValidateMediaItem_endAndStartPositionUs() { - const mediaItem = createDrmMedia(); - - mediaItem.endPositionUs = 0; - mediaItem.startPositionUs = 120 * 1000; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.endPositionUs = '0'; - assertFalse(validation.validateMediaItem(mediaItem)); - - mediaItem.endPositionUs = 0; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.startPositionUs = true; - assertFalse(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validation of the title. */ - testValidateMediaItem_title() { - const mediaItem = createDrmMedia(); - - mediaItem.title = '0'; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.title = 0; - assertFalse(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validation of the description. */ - testValidateMediaItem_description() { - const mediaItem = createDrmMedia(); - - mediaItem.description = '0'; - assertTrue(validation.validateMediaItem(mediaItem)); - - mediaItem.description = 0; - assertFalse(validation.validateMediaItem(mediaItem)); - }, - - /** Tests validating property of type string. */ - testValidateProperty_string() { - const obj = { - field: 'string', - }; - assertTrue(validation.validateProperty(obj, 'field', 'string')); - assertTrue(validation.validateProperty(obj, 'field', '?string')); - - obj.field = 0; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertFalse(validation.validateProperty(obj, 'field', '?string')); - - obj.field = true; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertFalse(validation.validateProperty(obj, 'field', '?string')); - - obj.field = {}; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertFalse(validation.validateProperty(obj, 'field', '?string')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'string')); - assertTrue(validation.validateProperty(obj, 'field', '?string')); - }, - - /** Tests validating property of type number. */ - testValidateProperty_number() { - const obj = { - field: 0, - }; - assertTrue(validation.validateProperty(obj, 'field', 'number')); - assertTrue(validation.validateProperty(obj, 'field', '?number')); - - obj.field = '0'; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertFalse(validation.validateProperty(obj, 'field', '?number')); - - obj.field = true; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertFalse(validation.validateProperty(obj, 'field', '?number')); - - obj.field = {}; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertFalse(validation.validateProperty(obj, 'field', '?number')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'number')); - assertTrue(validation.validateProperty(obj, 'field', '?number')); - }, - - /** Tests validating property of type boolean. */ - testValidateProperty_boolean() { - const obj = { - field: true, - }; - assertTrue(validation.validateProperty(obj, 'field', 'boolean')); - assertTrue(validation.validateProperty(obj, 'field', '?boolean')); - - obj.field = '0'; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertFalse(validation.validateProperty(obj, 'field', '?boolean')); - - obj.field = 1000; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertFalse(validation.validateProperty(obj, 'field', '?boolean')); - - obj.field = [true]; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertFalse(validation.validateProperty(obj, 'field', '?boolean')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'boolean')); - assertTrue(validation.validateProperty(obj, 'field', '?boolean')); - }, - - /** Tests validating property of type array. */ - testValidateProperty_array() { - const obj = { - field: [], - }; - assertTrue(validation.validateProperty(obj, 'field', 'Array')); - assertTrue(validation.validateProperty(obj, 'field', '?Array')); - - obj.field = '0'; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertFalse(validation.validateProperty(obj, 'field', '?Array')); - - obj.field = 1000; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertFalse(validation.validateProperty(obj, 'field', '?Array')); - - obj.field = true; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertFalse(validation.validateProperty(obj, 'field', '?Array')); - - delete obj.field; - assertFalse(validation.validateProperty(obj, 'field', 'Array')); - assertTrue(validation.validateProperty(obj, 'field', '?Array')); - }, - - /** Tests validating properties of type RepeatMode */ - testValidateProperty_repeatMode() { - const obj = { - off: 'OFF', - one: 'ONE', - all: 'ALL', - invalid: 'invalid', - }; - assertTrue(validation.validateProperty(obj, 'off', 'RepeatMode')); - assertTrue(validation.validateProperty(obj, 'one', 'RepeatMode')); - assertTrue(validation.validateProperty(obj, 'all', 'RepeatMode')); - assertFalse(validation.validateProperty(obj, 'invalid', 'RepeatMode')); - }, -}); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java deleted file mode 100644 index bc38cbdb8a..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ /dev/null @@ -1,437 +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.castdemo; - -import android.content.Context; -import android.net.Uri; -import androidx.annotation.Nullable; -import android.view.KeyEvent; -import android.view.View; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -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.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaQueueItem; -import com.google.android.gms.cast.framework.CastContext; -import java.util.ArrayList; -import org.json.JSONException; -import org.json.JSONObject; - -/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ class DefaultReceiverPlayerManager - implements PlayerManager, EventListener, SessionAvailabilityListener { - - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT); - - private final PlayerView localPlayerView; - private final PlayerControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final CastPlayer castPlayer; - private final ArrayList mediaQueue; - private final Listener listener; - private final ConcatenatingMediaSource concatenatingMediaSource; - - private int currentItemIndex; - private Player currentPlayer; - - /** - * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. - * - * @param listener A {@link Listener} for queue position changes. - * @param localPlayerView The {@link PlayerView} for local playback. - * @param castControlView The {@link PlayerControlView} to control remote playback. - * @param context A {@link Context}. - * @param castContext The {@link CastContext}. - */ - public DefaultReceiverPlayerManager( - Listener listener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.listener = listener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); - - castPlayer = new CastPlayer(castContext); - castPlayer.addListener(this); - castPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(castPlayer); - - setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); - } - - // Queue manipulation methods. - - /** - * Plays a specified queue item in the current player. - * - * @param itemIndex The index of the item to play. - */ - @Override - public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); - } - - /** Returns the index of the currently played item. */ - @Override - public int getCurrentItemIndex() { - return currentItemIndex; - } - - /** - * Appends {@code item} to the media queue. - * - * @param item The {@link MediaItem} to append. - */ - @Override - public void addItem(MediaItem item) { - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); - } - } - - /** Returns the size of the media queue. */ - @Override - public int getMediaQueueSize() { - return mediaQueue.size(); - } - - /** - * Returns the item at the given index in the media queue. - * - * @param position The index of the item. - * @return The item at the given index in the media queue. - */ - @Override - public MediaItem getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param item The item to remove. - * @return Whether the removal was successful. - */ - @Override - public boolean removeItem(MediaItem item) { - int itemIndex = mediaQueue.indexOf(item); - if (itemIndex == -1) { - return false; - } - concatenatingMediaSource.removeMediaSource(itemIndex); - if (currentPlayer == castPlayer) { - if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - if (castTimeline.getPeriodCount() <= itemIndex) { - return false; - } - castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); - } - } - mediaQueue.remove(itemIndex); - if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { - maybeSetCurrentItemAndNotify(C.INDEX_UNSET); - } else if (itemIndex < currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } - return true; - } - - /** - * Moves an item within the queue. - * - * @param item The item to move. - * @param toIndex The target index of the item in the queue. - * @return Whether the item move was successful. - */ - @Override - public boolean moveItem(MediaItem item, int toIndex) { - int fromIndex = mediaQueue.indexOf(item); - if (fromIndex == -1) { - return false; - } - // Player update. - concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); - if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { - Timeline castTimeline = castPlayer.getCurrentTimeline(); - int periodCount = castTimeline.getPeriodCount(); - if (periodCount <= fromIndex || periodCount <= toIndex) { - return false; - } - int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; - castPlayer.moveItem(elementId, toIndex); - } - - mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); - - // Index update. - if (fromIndex == currentItemIndex) { - maybeSetCurrentItemAndNotify(toIndex); - } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex + 1); - } - - return true; - } - - /** - * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. - * - * @param event The {@link KeyEvent}. - * @return Whether the event was handled by the target view. - */ - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (currentPlayer == exoPlayer) { - return localPlayerView.dispatchKeyEvent(event); - } else /* currentPlayer == castPlayer */ { - return castControlView.dispatchKeyEvent(event); - } - } - - /** Releases the manager and the players that it holds. */ - @Override - public void release() { - currentItemIndex = C.INDEX_UNSET; - mediaQueue.clear(); - concatenatingMediaSource.clear(); - castPlayer.setSessionAvailabilityListener(null); - castPlayer.release(); - localPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // Player.EventListener implementation. - - @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - updateCurrentItemIndex(); - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - updateCurrentItemIndex(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { - updateCurrentItemIndex(); - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(castPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // Internal methods. - - private void updateCurrentItemIndex() { - int playbackState = currentPlayer.getPlaybackState(); - maybeSetCurrentItemAndNotify( - playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED - ? currentPlayer.getCurrentWindowIndex() - : C.INDEX_UNSET); - } - - private void setCurrentPlayer(Player currentPlayer) { - if (this.currentPlayer == currentPlayer) { - return; - } - - // View management. - if (currentPlayer == exoPlayer) { - localPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else /* currentPlayer == castPlayer */ { - localPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - // Player state management. - long playbackPositionMs = C.TIME_UNSET; - int windowIndex = C.INDEX_UNSET; - boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); - if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.getCurrentWindowIndex(); - if (windowIndex != currentItemIndex) { - playbackPositionMs = C.TIME_UNSET; - windowIndex = currentItemIndex; - } - } - this.currentPlayer.stop(true); - } else { - // This is the initial setup. No need to save any state. - } - - this.currentPlayer = currentPlayer; - - // Media queue management. - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - } - - // Playback transition. - if (windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } - } - - /** - * Starts playback of the item at the given position. - * - * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { - maybeSetCurrentItemAndNotify(itemIndex); - if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { - MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; - for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); - } - castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); - } else { - currentPlayer.seekTo(itemIndex, positionMs); - currentPlayer.setPlayWhenReady(playWhenReady); - } - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + item.mimeType); - } - } - } - - private static MediaQueueItem buildMediaQueueItem(MediaItem item) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(item.media.uri.toString()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) - .setMetadata(movieMetadata); - if (!item.drmSchemes.isEmpty()) { - MediaItem.DrmScheme scheme = item.drmSchemes.get(0); - try { - // This configuration is only intended for testing and should *not* be used in production - // environments. See comment in the Cast Demo app's options provider. - JSONObject drmConfiguration = getDrmConfigurationJson(scheme); - if (drmConfiguration != null) { - mediaInfoBuilder.setCustomData(drmConfiguration); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); - } - - @Nullable - private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) - throws JSONException { - String drmScheme; - if (C.WIDEVINE_UUID.equals(scheme.uuid)) { - drmScheme = "widevine"; - } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { - drmScheme = "playready"; - } else { - return null; - } - MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); - JSONObject exoplayerConfig = - new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); - if (!licenseServer.uri.equals(Uri.EMPTY)) { - exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); - } - if (!licenseServer.requestHeaders.isEmpty()) { - exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); - } - return new JSONObject().put("exoPlayerConfig", exoplayerConfig); - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 9599da15cb..2d5a5f0ccf 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -98,6 +98,11 @@ import java.util.UUID; "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", "Clear DASH: Tears", MIME_TYPE_DASH)); + samples.add( + new Sample( + "https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8", + "Clear HLS: Angel one", + MIME_TYPE_HLS)); samples.add( new Sample( "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java deleted file mode 100644 index e8ad2c1a0d..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java +++ /dev/null @@ -1,421 +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.castdemo; - -import android.content.Context; -import android.net.Uri; -import androidx.annotation.Nullable; -import android.view.KeyEvent; -import android.view.View; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.ext.cast.DefaultCastSessionManager; -import com.google.android.exoplayer2.ext.cast.ExoCastPlayer; -import com.google.android.exoplayer2.ext.cast.MediaItem; -import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -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.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.gms.cast.framework.CastContext; -import java.util.ArrayList; - -/** Manages players and an internal media queue for the Cast demo app. */ -/* package */ class ExoCastPlayerManager - implements PlayerManager, EventListener, SessionAvailabilityListener { - - private static final String TAG = "ExoCastPlayerManager"; - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT); - - private final PlayerView localPlayerView; - private final PlayerControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final ExoCastPlayer exoCastPlayer; - private final ArrayList mediaQueue; - private final Listener listener; - private final ConcatenatingMediaSource concatenatingMediaSource; - - private int currentItemIndex; - private Player currentPlayer; - - /** - * Creates a new manager for {@link SimpleExoPlayer} and {@link ExoCastPlayer}. - * - * @param listener A {@link Listener}. - * @param localPlayerView The {@link PlayerView} for local playback. - * @param castControlView The {@link PlayerControlView} to control remote playback. - * @param context A {@link Context}. - * @param castContext The {@link CastContext}. - */ - public ExoCastPlayerManager( - Listener listener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.listener = listener; - this.localPlayerView = localPlayerView; - this.castControlView = castControlView; - mediaQueue = new ArrayList<>(); - currentItemIndex = C.INDEX_UNSET; - concatenatingMediaSource = new ConcatenatingMediaSource(); - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); - exoPlayer.addListener(this); - localPlayerView.setPlayer(exoPlayer); - - exoCastPlayer = - new ExoCastPlayer( - sessionManagerListener -> - new DefaultCastSessionManager(castContext, sessionManagerListener)); - exoCastPlayer.addListener(this); - exoCastPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(exoCastPlayer); - - setCurrentPlayer(exoCastPlayer.isCastSessionAvailable() ? exoCastPlayer : exoPlayer); - } - - // Queue manipulation methods. - - /** - * Plays a specified queue item in the current player. - * - * @param itemIndex The index of the item to play. - */ - @Override - public void selectQueueItem(int itemIndex) { - setCurrentItem(itemIndex, C.TIME_UNSET, true); - } - - /** Returns the index of the currently played item. */ - @Override - public int getCurrentItemIndex() { - return currentItemIndex; - } - - /** - * Appends {@code item} to the media queue. - * - * @param item The {@link MediaItem} to append. - */ - @Override - public void addItem(MediaItem item) { - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.addItemsToQueue(item); - } - } - - /** Returns the size of the media queue. */ - @Override - public int getMediaQueueSize() { - return mediaQueue.size(); - } - - /** - * Returns the item at the given index in the media queue. - * - * @param position The index of the item. - * @return The item at the given index in the media queue. - */ - @Override - public MediaItem getItem(int position) { - return mediaQueue.get(position); - } - - /** - * Removes the item at the given index from the media queue. - * - * @param item The item to remove. - * @return Whether the removal was successful. - */ - @Override - public boolean removeItem(MediaItem item) { - int itemIndex = mediaQueue.indexOf(item); - if (itemIndex == -1) { - // This may happen if another sender app removes items while this sender app is in "swiping - // an item" state. - return false; - } - concatenatingMediaSource.removeMediaSource(itemIndex); - mediaQueue.remove(itemIndex); - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.removeItemFromQueue(itemIndex); - } - if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { - maybeSetCurrentItemAndNotify(C.INDEX_UNSET); - } else if (itemIndex < currentItemIndex) { - maybeSetCurrentItemAndNotify(currentItemIndex - 1); - } - return true; - } - - /** - * Moves an item within the queue. - * - * @param item The item to move. This method does nothing if {@code item} is not contained in the - * queue. - * @param toIndex The target index of the item in the queue. If {@code toIndex} exceeds the last - * position in the queue, {@code toIndex} is clamped to match the largest possible value. - * @return True if {@code item} was contained in the queue, and {@code toIndex} was a valid - * position. False otherwise. - */ - @Override - public boolean moveItem(MediaItem item, int toIndex) { - int indexOfItem = mediaQueue.indexOf(item); - if (indexOfItem == -1) { - // This may happen if another sender app removes items while this sender app is in "dragging - // an item" state. - return false; - } - int clampedToIndex = Math.min(toIndex, mediaQueue.size() - 1); - mediaQueue.add(clampedToIndex, mediaQueue.remove(indexOfItem)); - concatenatingMediaSource.moveMediaSource(indexOfItem, clampedToIndex); - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.moveItemInQueue(indexOfItem, clampedToIndex); - } - // Index update. - maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); - return clampedToIndex == toIndex; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (currentPlayer == exoPlayer) { - return localPlayerView.dispatchKeyEvent(event); - } else /* currentPlayer == exoCastPlayer */ { - return castControlView.dispatchKeyEvent(event); - } - } - - /** Releases the manager and the players that it holds. */ - @Override - public void release() { - currentItemIndex = C.INDEX_UNSET; - mediaQueue.clear(); - concatenatingMediaSource.clear(); - exoCastPlayer.setSessionAvailabilityListener(null); - exoCastPlayer.release(); - localPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // Player.EventListener implementation. - - @Override - public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - updateCurrentItemIndex(); - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - updateCurrentItemIndex(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { - if (currentPlayer == exoCastPlayer && reason != Player.TIMELINE_CHANGE_REASON_RESET) { - maybeUpdateLocalQueueWithRemoteQueueAndNotify(); - } - updateCurrentItemIndex(); - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - Log.e(TAG, "The player encountered an error.", error); - listener.onPlayerError(); - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setCurrentPlayer(exoCastPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setCurrentPlayer(exoPlayer); - } - - // Internal methods. - - private void maybeUpdateLocalQueueWithRemoteQueueAndNotify() { - Assertions.checkState(currentPlayer == exoCastPlayer); - boolean mediaQueuesMatch = mediaQueue.size() == exoCastPlayer.getQueueSize(); - for (int i = 0; mediaQueuesMatch && i < mediaQueue.size(); i++) { - mediaQueuesMatch = mediaQueue.get(i).uuid.equals(exoCastPlayer.getQueueItem(i).uuid); - } - if (mediaQueuesMatch) { - // The media queues match. Do nothing. - return; - } - mediaQueue.clear(); - concatenatingMediaSource.clear(); - for (int i = 0; i < exoCastPlayer.getQueueSize(); i++) { - MediaItem item = exoCastPlayer.getQueueItem(i); - mediaQueue.add(item); - concatenatingMediaSource.addMediaSource(buildMediaSource(item)); - } - listener.onQueueContentsExternallyChanged(); - } - - private void updateCurrentItemIndex() { - int playbackState = currentPlayer.getPlaybackState(); - maybeSetCurrentItemAndNotify( - playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED - ? currentPlayer.getCurrentWindowIndex() - : C.INDEX_UNSET); - } - - private void setCurrentPlayer(Player currentPlayer) { - if (this.currentPlayer == currentPlayer) { - return; - } - - // View management. - if (currentPlayer == exoPlayer) { - localPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else /* currentPlayer == exoCastPlayer */ { - localPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - // Player state management. - long playbackPositionMs = C.TIME_UNSET; - int windowIndex = C.INDEX_UNSET; - boolean playWhenReady = false; - if (this.currentPlayer != null) { - int playbackState = this.currentPlayer.getPlaybackState(); - if (playbackState != Player.STATE_ENDED) { - playbackPositionMs = this.currentPlayer.getCurrentPosition(); - playWhenReady = this.currentPlayer.getPlayWhenReady(); - windowIndex = this.currentPlayer.getCurrentWindowIndex(); - if (windowIndex != currentItemIndex) { - playbackPositionMs = C.TIME_UNSET; - windowIndex = currentItemIndex; - } - } - this.currentPlayer.stop(true); - } else { - // This is the initial setup. No need to save any state. - } - - this.currentPlayer = currentPlayer; - - // Media queue management. - boolean shouldSeekInNewCurrentPlayer; - if (currentPlayer == exoPlayer) { - exoPlayer.prepare(concatenatingMediaSource); - shouldSeekInNewCurrentPlayer = true; - } else /* currentPlayer == exoCastPlayer */ { - if (exoCastPlayer.getPlaybackState() == Player.STATE_IDLE) { - exoCastPlayer.prepare(); - } - if (mediaQueue.isEmpty()) { - // Casting started with no local queue. We take the receiver app's queue as our own. - maybeUpdateLocalQueueWithRemoteQueueAndNotify(); - shouldSeekInNewCurrentPlayer = false; - } else { - // Casting started when the sender app had no queue. We just load our items into the - // receiver app's queue. If the receiver had no items in its queue, we also seek to wherever - // the sender app was playing. - int currentExoCastPlayerState = exoCastPlayer.getPlaybackState(); - shouldSeekInNewCurrentPlayer = - currentExoCastPlayerState == Player.STATE_IDLE - || currentExoCastPlayerState == Player.STATE_ENDED; - exoCastPlayer.addItemsToQueue(mediaQueue.toArray(new MediaItem[0])); - } - } - - // Playback transition. - if (shouldSeekInNewCurrentPlayer && windowIndex != C.INDEX_UNSET) { - setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); - } else if (getMediaQueueSize() > 0) { - maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex()); - } - } - - /** - * Starts playback of the item at the given position. - * - * @param itemIndex The index of the item to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { - maybeSetCurrentItemAndNotify(itemIndex); - currentPlayer.seekTo(itemIndex, positionMs); - if (currentPlayer.getPlaybackState() == Player.STATE_IDLE) { - if (currentPlayer == exoCastPlayer) { - exoCastPlayer.prepare(); - } else { - exoPlayer.prepare(concatenatingMediaSource); - } - } - currentPlayer.setPlayWhenReady(playWhenReady); - } - - private void maybeSetCurrentItemAndNotify(int currentItemIndex) { - if (this.currentItemIndex != currentItemIndex) { - int oldIndex = this.currentItemIndex; - this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); - } - } - - private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + item.mimeType); - } - } - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 5ed434eed6..c17c0a62ab 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -34,14 +34,11 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; @@ -120,21 +117,13 @@ public class MainActivity extends AppCompatActivity // There is no Cast context to work with. Do nothing. return; } - String applicationId = castContext.getCastOptions().getReceiverApplicationId(); - switch (applicationId) { - case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: - case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM: - playerManager = - new DefaultReceiverPlayerManager( - /* listener= */ this, - localPlayerView, - castControlView, - /* context= */ this, - castContext); - break; - default: - throw new IllegalStateException("Illegal receiver app id: " + applicationId); - } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); mediaQueueList.setAdapter(mediaQueueListAdapter); } @@ -181,16 +170,6 @@ public class MainActivity extends AppCompatActivity } } - @Override - public void onQueueContentsExternallyChanged() { - mediaQueueListAdapter.notifyDataSetChanged(); - } - - @Override - public void onPlayerError() { - Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show(); - } - // Internal methods. private View buildSampleListView() { diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index c9a728b3ff..c92ebd7e94 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -15,53 +15,419 @@ */ package com.google.android.exoplayer2.castdemo; +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; import android.view.KeyEvent; +import android.view.View; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +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.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; -/** Manages the players in the Cast demo app. */ -/* package */ interface PlayerManager { +/** Manages players and an internal media queue for the demo app. */ +/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { /** Listener for events. */ interface Listener { /** Called when the currently played item of the media queue changes. */ void onQueuePositionChanged(int previousIndex, int newIndex); - - /** Called when the media queue changes due to modifications not caused by this manager. */ - void onQueueContentsExternallyChanged(); - - /** Called when an error occurs in the current player. */ - void onPlayerError(); } - /** Redirects the given {@code keyEvent} to the active player. */ - boolean dispatchKeyEvent(KeyEvent keyEvent); + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); - /** Appends the given {@link MediaItem} to the media queue. */ - void addItem(MediaItem mediaItem); + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; - /** Returns the number of items in the media queue. */ - int getMediaQueueSize(); - - /** Selects the item at the given position for playback. */ - void selectQueueItem(int position); + private int currentItemIndex; + private Player currentPlayer; /** - * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is - * being played. + * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. + * + * @param listener A {@link Listener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. + * @param context A {@link Context}. + * @param castContext The {@link CastContext}. */ - int getCurrentItemIndex(); + public PlayerManager( + Listener listener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.listener = listener; + this.localPlayerView = localPlayerView; + this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; + concatenatingMediaSource = new ConcatenatingMediaSource(); - /** Returns the {@link MediaItem} at the given {@code position}. */ - MediaItem getItem(int position); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + exoPlayer.addListener(this); + localPlayerView.setPlayer(exoPlayer); - /** Moves the item at position {@code from} to position {@code to}. */ - boolean moveItem(MediaItem item, int to); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); - /** Removes the item at position {@code index}. */ - boolean removeItem(MediaItem item); + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } - /** Releases any acquired resources. */ - void release(); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); + } + + /** Returns the index of the currently played item. */ + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code item} to the media queue. + * + * @param item The {@link MediaItem} to append. + */ + public void addItem(MediaItem item) { + mediaQueue.add(item); + concatenatingMediaSource.addMediaSource(buildMediaSource(item)); + if (currentPlayer == castPlayer) { + castPlayer.addItems(buildMediaQueueItem(item)); + } + } + + /** Returns the size of the media queue. */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public MediaItem getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param item The item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(MediaItem item) { + int itemIndex = mediaQueue.indexOf(item); + if (itemIndex == -1) { + return false; + } + concatenatingMediaSource.removeMediaSource(itemIndex); + if (currentPlayer == castPlayer) { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param item The item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(MediaItem item, int toIndex) { + int fromIndex = mediaQueue.indexOf(item); + if (fromIndex == -1) { + return false; + } + // Player update. + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (currentPlayer == exoPlayer) { + return localPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); + } + } + + /** Releases the manager and the players that it holds. */ + public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); + concatenatingMediaSource.clear(); + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + localPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + updateCurrentItemIndex(); + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setCurrentPlayer(castPlayer); + } + + @Override + public void onCastSessionUnavailable() { + setCurrentPlayer(exoPlayer); + } + + // Internal methods. + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() + : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + localPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + localPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + + Player previousPlayer = this.currentPlayer; + if (previousPlayer != null) { + // Save state from the previous player. + int playbackState = previousPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = previousPlayer.getCurrentPosition(); + playWhenReady = previousPlayer.getPlayWhenReady(); + windowIndex = previousPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + previousPlayer.stop(true); + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + if (currentPlayer == exoPlayer) { + exoPlayer.prepare(concatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } + + private static MediaSource buildMediaSource(MediaItem item) { + Uri uri = item.media.uri; + switch (item.mimeType) { + case DemoUtil.MIME_TYPE_SS: + return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_DASH: + return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + case DemoUtil.MIME_TYPE_VIDEO_MP4: + return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + default: + throw new IllegalStateException("Unsupported type: " + item.mimeType); + } + } + + private static MediaQueueItem buildMediaQueueItem(MediaItem item) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); + MediaInfo.Builder mediaInfoBuilder = + new MediaInfo.Builder(item.media.uri.toString()) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setContentType(item.mimeType) + .setMetadata(movieMetadata); + if (!item.drmSchemes.isEmpty()) { + MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + try { + // This configuration is only intended for testing and should *not* be used in production + // environments. See comment in the Cast Demo app's options provider. + JSONObject drmConfiguration = getDrmConfigurationJson(scheme); + if (drmConfiguration != null) { + mediaInfoBuilder.setCustomData(drmConfiguration); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); + } + + @Nullable + private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + throws JSONException { + String drmScheme; + if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + drmScheme = "playready"; + } else { + return null; + } + MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); + JSONObject exoplayerConfig = + new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); + if (!licenseServer.uri.equals(Uri.EMPTY)) { + exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + } + if (!licenseServer.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + } + return new JSONObject().put("exoPlayerConfig", exoplayerConfig); + } } diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 013b50a175..2f0acd4808 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -24,6 +24,4 @@ Failed to get Cast context. Try updating Google Play Services and restart the app. - Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info. - diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java deleted file mode 100644 index 7c1f06e8d2..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java +++ /dev/null @@ -1,86 +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; - -/** Handles communication with the receiver app using a cast session. */ -public interface CastSessionManager { - - /** Factory for {@link CastSessionManager} instances. */ - interface Factory { - - /** - * Creates a {@link CastSessionManager} instance with the given listener. - * - * @param listener The listener to notify on receiver app and session state updates. - * @return The created instance. - */ - CastSessionManager create(StateListener listener); - } - - /** - * Extends {@link SessionAvailabilityListener} by adding receiver app state notifications. - * - *

        Receiver app state notifications contain a sequence number that matches the sequence number - * of the last {@link ExoCastMessage} sent (using {@link #send(ExoCastMessage)}) by this session - * manager and processed by the receiver app. Sequence numbers are non-negative numbers. - */ - interface StateListener extends SessionAvailabilityListener { - - /** - * Called when a status update is received from the Cast Receiver app. - * - * @param stateUpdate A {@link ReceiverAppStateUpdate} containing the fields included in the - * message. - */ - void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate); - } - - /** - * Special constant representing an unset sequence number. It is guaranteed to be a negative - * value. - */ - long SEQUENCE_NUMBER_UNSET = Long.MIN_VALUE; - - /** - * Connects the session manager to the cast message bus and starts listening for session - * availability changes. Also announces that this sender app is connected to the message bus. - */ - void start(); - - /** Stops tracking the state of the cast session and closes any existing session. */ - void stopTrackingSession(); - - /** - * Same as {@link #stopTrackingSession()}, but also stops the receiver app if a session is - * currently available. - */ - void stopTrackingSessionAndCasting(); - - /** Whether a cast session is available. */ - boolean isCastSessionAvailable(); - - /** - * Sends an {@link ExoCastMessage} to the receiver app. - * - *

        A sequence number is assigned to every sent message. Message senders may mask the local - * state until a status update from the receiver app (see {@link StateListener}) is received with - * a greater or equal sequence number. - * - * @param message The message to send. - * @return The sequence number assigned to the message. - */ - long send(ExoCastMessage message); -} 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 5aed1373e5..8948173f60 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 @@ -44,7 +44,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider { * 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: + // 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"; diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java deleted file mode 100644 index c08a9bc352..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java +++ /dev/null @@ -1,187 +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; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.framework.CastContext; -import com.google.android.gms.cast.framework.CastSession; -import com.google.android.gms.cast.framework.SessionManager; -import com.google.android.gms.cast.framework.SessionManagerListener; -import java.io.IOException; -import org.json.JSONException; - -/** Implements {@link CastSessionManager} by using JSON message passing. */ -public class DefaultCastSessionManager implements CastSessionManager { - - private static final String TAG = "DefaultCastSessionManager"; - private static final String EXOPLAYER_CAST_NAMESPACE = "urn:x-cast:com.google.exoplayer.cast"; - - private final SessionManager sessionManager; - private final CastSessionListener castSessionListener; - private final StateListener stateListener; - private final Cast.MessageReceivedCallback messageReceivedCallback; - - private boolean started; - private long sequenceNumber; - private long expectedInitialStateUpdateSequence; - @Nullable private CastSession currentSession; - - /** - * @param context The Cast context from which the cast session is obtained. - * @param stateListener The listener to notify of state changes. - */ - public DefaultCastSessionManager(CastContext context, StateListener stateListener) { - this.stateListener = stateListener; - sessionManager = context.getSessionManager(); - currentSession = sessionManager.getCurrentCastSession(); - castSessionListener = new CastSessionListener(); - messageReceivedCallback = new CastMessageCallback(); - expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; - } - - @Override - public void start() { - started = true; - sessionManager.addSessionManagerListener(castSessionListener, CastSession.class); - currentSession = sessionManager.getCurrentCastSession(); - if (currentSession != null) { - setMessageCallbackOnSession(); - } - } - - @Override - public void stopTrackingSession() { - stop(/* stopCasting= */ false); - } - - @Override - public void stopTrackingSessionAndCasting() { - stop(/* stopCasting= */ true); - } - - @Override - public boolean isCastSessionAvailable() { - return currentSession != null && expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET; - } - - @Override - public long send(ExoCastMessage message) { - if (currentSession != null) { - currentSession.sendMessage(EXOPLAYER_CAST_NAMESPACE, message.toJsonString(sequenceNumber)); - } else { - Log.w(TAG, "Tried to send a message with no established session. Method: " + message.method); - } - return sequenceNumber++; - } - - private void stop(boolean stopCasting) { - sessionManager.removeSessionManagerListener(castSessionListener, CastSession.class); - if (currentSession != null) { - sessionManager.endCurrentSession(stopCasting); - } - currentSession = null; - started = false; - } - - private void setCastSession(@Nullable CastSession session) { - Assertions.checkState(started); - boolean hadSession = currentSession != null; - currentSession = session; - if (!hadSession && session != null) { - setMessageCallbackOnSession(); - } else if (hadSession && session == null) { - stateListener.onCastSessionUnavailable(); - } - } - - private void setMessageCallbackOnSession() { - try { - Assertions.checkNotNull(currentSession) - .setMessageReceivedCallbacks(EXOPLAYER_CAST_NAMESPACE, messageReceivedCallback); - expectedInitialStateUpdateSequence = send(new ExoCastMessage.OnClientConnected()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - - /** Listens for Cast session state changes. */ - private class CastSessionListener implements SessionManagerListener { - - @Override - public void onSessionStarting(CastSession castSession) {} - - @Override - public void onSessionStarted(CastSession castSession, String sessionId) { - setCastSession(castSession); - } - - @Override - public void onSessionStartFailed(CastSession castSession, int error) {} - - @Override - public void onSessionEnding(CastSession castSession) {} - - @Override - public void onSessionEnded(CastSession castSession, int error) { - setCastSession(null); - } - - @Override - public void onSessionResuming(CastSession castSession, String sessionId) {} - - @Override - public void onSessionResumed(CastSession castSession, boolean wasSuspended) { - setCastSession(castSession); - } - - @Override - public void onSessionResumeFailed(CastSession castSession, int error) {} - - @Override - public void onSessionSuspended(CastSession castSession, int reason) { - setCastSession(null); - } - } - - private class CastMessageCallback implements Cast.MessageReceivedCallback { - - @Override - public void onMessageReceived(CastDevice castDevice, String namespace, String message) { - if (!EXOPLAYER_CAST_NAMESPACE.equals(namespace)) { - // Non-matching namespace. Ignore. - Log.e(TAG, String.format("Unrecognized namespace: '%s'.", namespace)); - return; - } - try { - ReceiverAppStateUpdate receivedUpdate = ReceiverAppStateUpdate.fromJsonMessage(message); - if (expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET - || receivedUpdate.sequenceNumber >= expectedInitialStateUpdateSequence) { - stateListener.onStateUpdateFromReceiverApp(receivedUpdate); - if (expectedInitialStateUpdateSequence != SEQUENCE_NUMBER_UNSET) { - expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET; - stateListener.onCastSessionAvailable(); - } - } - } catch (JSONException e) { - Log.e(TAG, "Error while parsing state update from receiver: ", e); - } - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java deleted file mode 100644 index 36173bfc5d..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java +++ /dev/null @@ -1,118 +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; - -/** Defines constants used by the Cast extension. */ -public final class ExoCastConstants { - - private ExoCastConstants() {} - - public static final int PROTOCOL_VERSION = 0; - - // String representations. - - public static final String STR_STATE_IDLE = "IDLE"; - public static final String STR_STATE_BUFFERING = "BUFFERING"; - public static final String STR_STATE_READY = "READY"; - public static final String STR_STATE_ENDED = "ENDED"; - - public static final String STR_REPEAT_MODE_OFF = "OFF"; - public static final String STR_REPEAT_MODE_ONE = "ONE"; - public static final String STR_REPEAT_MODE_ALL = "ALL"; - - public static final String STR_DISCONTINUITY_REASON_PERIOD_TRANSITION = "PERIOD_TRANSITION"; - public static final String STR_DISCONTINUITY_REASON_SEEK = "SEEK"; - public static final String STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT = "SEEK_ADJUSTMENT"; - public static final String STR_DISCONTINUITY_REASON_AD_INSERTION = "AD_INSERTION"; - public static final String STR_DISCONTINUITY_REASON_INTERNAL = "INTERNAL"; - - public static final String STR_SELECTION_FLAG_DEFAULT = "DEFAULT"; - public static final String STR_SELECTION_FLAG_FORCED = "FORCED"; - public static final String STR_SELECTION_FLAG_AUTOSELECT = "AUTOSELECT"; - - // Methods. - - public static final String METHOD_BASE = "player."; - - public static final String METHOD_ON_CLIENT_CONNECTED = METHOD_BASE + "onClientConnected"; - public static final String METHOD_ADD_ITEMS = METHOD_BASE + "addItems"; - public static final String METHOD_MOVE_ITEM = METHOD_BASE + "moveItem"; - public static final String METHOD_PREPARE = METHOD_BASE + "prepare"; - public static final String METHOD_REMOVE_ITEMS = METHOD_BASE + "removeItems"; - public static final String METHOD_SET_PLAY_WHEN_READY = METHOD_BASE + "setPlayWhenReady"; - public static final String METHOD_SET_REPEAT_MODE = METHOD_BASE + "setRepeatMode"; - public static final String METHOD_SET_SHUFFLE_MODE_ENABLED = - METHOD_BASE + "setShuffleModeEnabled"; - public static final String METHOD_SEEK_TO = METHOD_BASE + "seekTo"; - public static final String METHOD_SET_PLAYBACK_PARAMETERS = METHOD_BASE + "setPlaybackParameters"; - public static final String METHOD_SET_TRACK_SELECTION_PARAMETERS = - METHOD_BASE + ".setTrackSelectionParameters"; - public static final String METHOD_STOP = METHOD_BASE + "stop"; - - // JSON message keys. - - public static final String KEY_ARGS = "args"; - public static final String KEY_DEFAULT_START_POSITION_US = "defaultStartPositionUs"; - public static final String KEY_DESCRIPTION = "description"; - public static final String KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS = - "disabledTextTrackSelectionFlags"; - public static final String KEY_DISCONTINUITY_REASON = "discontinuityReason"; - public static final String KEY_DRM_SCHEMES = "drmSchemes"; - public static final String KEY_DURATION_US = "durationUs"; - public static final String KEY_END_POSITION_US = "endPositionUs"; - public static final String KEY_ERROR_MESSAGE = "error"; - public static final String KEY_ID = "id"; - public static final String KEY_INDEX = "index"; - public static final String KEY_IS_DYNAMIC = "isDynamic"; - public static final String KEY_IS_LOADING = "isLoading"; - public static final String KEY_IS_SEEKABLE = "isSeekable"; - public static final String KEY_ITEMS = "items"; - public static final String KEY_LICENSE_SERVER = "licenseServer"; - public static final String KEY_MEDIA = "media"; - public static final String KEY_MEDIA_ITEMS_INFO = "mediaItemsInfo"; - public static final String KEY_MEDIA_QUEUE = "mediaQueue"; - public static final String KEY_METHOD = "method"; - public static final String KEY_MIME_TYPE = "mimeType"; - public static final String KEY_PERIOD_ID = "periodId"; - public static final String KEY_PERIODS = "periods"; - public static final String KEY_PITCH = "pitch"; - public static final String KEY_PLAY_WHEN_READY = "playWhenReady"; - public static final String KEY_PLAYBACK_PARAMETERS = "playbackParameters"; - public static final String KEY_PLAYBACK_POSITION = "playbackPosition"; - public static final String KEY_PLAYBACK_STATE = "playbackState"; - public static final String KEY_POSITION_IN_FIRST_PERIOD_US = "positionInFirstPeriodUs"; - public static final String KEY_POSITION_MS = "positionMs"; - public static final String KEY_PREFERRED_AUDIO_LANGUAGE = "preferredAudioLanguage"; - public static final String KEY_PREFERRED_TEXT_LANGUAGE = "preferredTextLanguage"; - public static final String KEY_PROTOCOL_VERSION = "protocolVersion"; - public static final String KEY_REPEAT_MODE = "repeatMode"; - public static final String KEY_REQUEST_HEADERS = "requestHeaders"; - public static final String KEY_RESET = "reset"; - public static final String KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE = - "selectUndeterminedTextLanguage"; - public static final String KEY_SEQUENCE_NUMBER = "sequenceNumber"; - public static final String KEY_SHUFFLE_MODE_ENABLED = "shuffleModeEnabled"; - public static final String KEY_SHUFFLE_ORDER = "shuffleOrder"; - public static final String KEY_SKIP_SILENCE = "skipSilence"; - public static final String KEY_SPEED = "speed"; - public static final String KEY_START_POSITION_US = "startPositionUs"; - public static final String KEY_TITLE = "title"; - public static final String KEY_TRACK_SELECTION_PARAMETERS = "trackSelectionParameters"; - public static final String KEY_URI = "uri"; - public static final String KEY_UUID = "uuid"; - public static final String KEY_UUIDS = "uuids"; - public static final String KEY_WINDOW_DURATION_US = "windowDurationUs"; -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java deleted file mode 100644 index 1529e9f5ac..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java +++ /dev/null @@ -1,474 +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; - -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PROTOCOL_VERSION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_RESET; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ON_CLIENT_CONNECTED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_PREPARE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_STOP; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.PROTOCOL_VERSION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_DEFAULT; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -// TODO(Internal b/118432277): Evaluate using a proto for sending to the receiver app. -/** A serializable message for operating a media player. */ -public abstract class ExoCastMessage { - - /** Notifies the receiver app of the connection of a sender app to the message bus. */ - public static final class OnClientConnected extends ExoCastMessage { - - public OnClientConnected() { - super(METHOD_ON_CLIENT_CONNECTED); - } - - @Override - protected JSONObject getArgumentsAsJsonObject() { - // No arguments needed. - return new JSONObject(); - } - } - - /** Transitions the player out of {@link Player#STATE_IDLE}. */ - public static final class Prepare extends ExoCastMessage { - - public Prepare() { - super(METHOD_PREPARE); - } - - @Override - protected JSONObject getArgumentsAsJsonObject() { - // No arguments needed. - return new JSONObject(); - } - } - - /** Transitions the player to {@link Player#STATE_IDLE} and optionally resets its state. */ - public static final class Stop extends ExoCastMessage { - - /** Whether the player state should be reset. */ - public final boolean reset; - - public Stop(boolean reset) { - super(METHOD_STOP); - this.reset = reset; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_RESET, reset); - } - } - - /** Adds items to a media player queue. */ - public static final class AddItems extends ExoCastMessage { - - /** - * The index at which the {@link #items} should be inserted. If {@link C#INDEX_UNSET}, the items - * are appended to the queue. - */ - public final int index; - /** The {@link MediaItem items} to add to the media queue. */ - public final List items; - /** - * The shuffle order to use for the media queue that results of adding the items to the queue. - */ - public final ShuffleOrder shuffleOrder; - - /** - * @param index See {@link #index}. - * @param items See {@link #items}. - * @param shuffleOrder See {@link #shuffleOrder}. - */ - public AddItems(int index, List items, ShuffleOrder shuffleOrder) { - super(METHOD_ADD_ITEMS); - this.index = index; - this.items = Collections.unmodifiableList(new ArrayList<>(items)); - this.shuffleOrder = shuffleOrder; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - JSONObject arguments = - new JSONObject() - .put(KEY_ITEMS, getItemsAsJsonArray()) - .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); - maybePutValue(arguments, KEY_INDEX, index, C.INDEX_UNSET); - return arguments; - } - - private JSONArray getItemsAsJsonArray() throws JSONException { - JSONArray result = new JSONArray(); - for (MediaItem item : items) { - result.put(mediaItemAsJsonObject(item)); - } - return result; - } - } - - /** Moves an item in a player media queue. */ - public static final class MoveItem extends ExoCastMessage { - - /** The {@link MediaItem#uuid} of the item to move. */ - public final UUID uuid; - /** The index in the queue to which the item should be moved. */ - public final int index; - /** The shuffle order to use for the media queue that results of moving the item. */ - public ShuffleOrder shuffleOrder; - - /** - * @param uuid See {@link #uuid}. - * @param index See {@link #index}. - * @param shuffleOrder See {@link #shuffleOrder}. - */ - public MoveItem(UUID uuid, int index, ShuffleOrder shuffleOrder) { - super(METHOD_MOVE_ITEM); - this.uuid = uuid; - this.index = index; - this.shuffleOrder = shuffleOrder; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject() - .put(KEY_UUID, uuid) - .put(KEY_INDEX, index) - .put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder)); - } - } - - /** Removes items from a player queue. */ - public static final class RemoveItems extends ExoCastMessage { - - /** The {@link MediaItem#uuid} of the items to remove from the queue. */ - public final List uuids; - - /** @param uuids See {@link #uuids}. */ - public RemoveItems(List uuids) { - super(METHOD_REMOVE_ITEMS); - this.uuids = Collections.unmodifiableList(new ArrayList<>(uuids)); - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_UUIDS, new JSONArray(uuids)); - } - } - - /** See {@link Player#setPlayWhenReady(boolean)}. */ - public static final class SetPlayWhenReady extends ExoCastMessage { - - /** The {@link Player#setPlayWhenReady(boolean) playWhenReady} value to set. */ - public final boolean playWhenReady; - - /** @param playWhenReady See {@link #playWhenReady}. */ - public SetPlayWhenReady(boolean playWhenReady) { - super(METHOD_SET_PLAY_WHEN_READY); - this.playWhenReady = playWhenReady; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_PLAY_WHEN_READY, playWhenReady); - } - } - - /** - * Sets the repeat mode of the media player. - * - * @see Player#setRepeatMode(int) - */ - public static final class SetRepeatMode extends ExoCastMessage { - - /** The {@link Player#setRepeatMode(int) repeatMode} to set. */ - @Player.RepeatMode public final int repeatMode; - - /** @param repeatMode See {@link #repeatMode}. */ - public SetRepeatMode(@Player.RepeatMode int repeatMode) { - super(METHOD_SET_REPEAT_MODE); - this.repeatMode = repeatMode; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_REPEAT_MODE, repeatModeToString(repeatMode)); - } - - private static String repeatModeToString(@Player.RepeatMode int repeatMode) { - switch (repeatMode) { - case REPEAT_MODE_OFF: - return STR_REPEAT_MODE_OFF; - case REPEAT_MODE_ONE: - return STR_REPEAT_MODE_ONE; - case REPEAT_MODE_ALL: - return STR_REPEAT_MODE_ALL; - default: - throw new AssertionError("Illegal repeat mode: " + repeatMode); - } - } - } - - /** - * Enables and disables shuffle mode in the media player. - * - * @see Player#setShuffleModeEnabled(boolean) - */ - public static final class SetShuffleModeEnabled extends ExoCastMessage { - - /** The {@link Player#setShuffleModeEnabled(boolean) shuffleModeEnabled} value to set. */ - public boolean shuffleModeEnabled; - - /** @param shuffleModeEnabled See {@link #shuffleModeEnabled}. */ - public SetShuffleModeEnabled(boolean shuffleModeEnabled) { - super(METHOD_SET_SHUFFLE_MODE_ENABLED); - this.shuffleModeEnabled = shuffleModeEnabled; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject().put(KEY_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); - } - } - - /** See {@link Player#seekTo(int, long)}. */ - public static final class SeekTo extends ExoCastMessage { - - /** The {@link MediaItem#uuid} of the item to seek to. */ - public final UUID uuid; - /** - * The seek position in milliseconds in the specified item. If {@link C#TIME_UNSET}, the target - * position is the item's default position. - */ - public final long positionMs; - - /** - * @param uuid See {@link #uuid}. - * @param positionMs See {@link #positionMs}. - */ - public SeekTo(UUID uuid, long positionMs) { - super(METHOD_SEEK_TO); - this.uuid = uuid; - this.positionMs = positionMs; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - JSONObject result = new JSONObject().put(KEY_UUID, uuid); - ExoCastMessage.maybePutValue(result, KEY_POSITION_MS, positionMs, C.TIME_UNSET); - return result; - } - } - - /** See {@link Player#setPlaybackParameters(PlaybackParameters)}. */ - public static final class SetPlaybackParameters extends ExoCastMessage { - - /** The {@link Player#setPlaybackParameters(PlaybackParameters) parameters} to set. */ - public final PlaybackParameters playbackParameters; - - /** @param playbackParameters See {@link #playbackParameters}. */ - public SetPlaybackParameters(PlaybackParameters playbackParameters) { - super(METHOD_SET_PLAYBACK_PARAMETERS); - this.playbackParameters = playbackParameters; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - return new JSONObject() - .put(KEY_SPEED, playbackParameters.speed) - .put(KEY_PITCH, playbackParameters.pitch) - .put(KEY_SKIP_SILENCE, playbackParameters.skipSilence); - } - } - - /** See {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters)}. */ - public static final class SetTrackSelectionParameters extends ExoCastMessage { - - /** - * The {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters) parameters} to - * set - */ - public final TrackSelectionParameters trackSelectionParameters; - - public SetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { - super(METHOD_SET_TRACK_SELECTION_PARAMETERS); - this.trackSelectionParameters = trackSelectionParameters; - } - - @Override - protected JSONObject getArgumentsAsJsonObject() throws JSONException { - JSONArray disabledTextSelectionFlagsJson = new JSONArray(); - int disabledSelectionFlags = trackSelectionParameters.disabledTextTrackSelectionFlags; - if ((disabledSelectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) { - disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_AUTOSELECT); - } - if ((disabledSelectionFlags & C.SELECTION_FLAG_FORCED) != 0) { - disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_FORCED); - } - if ((disabledSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) { - disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_DEFAULT); - } - return new JSONObject() - .put(KEY_PREFERRED_AUDIO_LANGUAGE, trackSelectionParameters.preferredAudioLanguage) - .put(KEY_PREFERRED_TEXT_LANGUAGE, trackSelectionParameters.preferredTextLanguage) - .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, disabledTextSelectionFlagsJson) - .put( - KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, - trackSelectionParameters.selectUndeterminedTextLanguage); - } - } - - public final String method; - - /** - * Creates a message with the given method. - * - * @param method The method of the message. - */ - protected ExoCastMessage(String method) { - this.method = method; - } - - /** - * Returns a string containing a JSON representation of this message. - * - * @param sequenceNumber The sequence number to associate with this message. - * @return A string containing a JSON representation of this message. - */ - public final String toJsonString(long sequenceNumber) { - try { - JSONObject message = - new JSONObject() - .put(KEY_PROTOCOL_VERSION, PROTOCOL_VERSION) - .put(KEY_METHOD, method) - .put(KEY_SEQUENCE_NUMBER, sequenceNumber) - .put(KEY_ARGS, getArgumentsAsJsonObject()); - return message.toString(); - } catch (JSONException e) { - throw new AssertionError(e); - } - } - - /** Returns a {@link JSONObject} representation of the given item. */ - protected static JSONObject mediaItemAsJsonObject(MediaItem item) throws JSONException { - JSONObject itemAsJson = new JSONObject(); - itemAsJson.put(KEY_UUID, item.uuid); - itemAsJson.put(KEY_TITLE, item.title); - itemAsJson.put(KEY_DESCRIPTION, item.description); - itemAsJson.put(KEY_MEDIA, uriBundleAsJsonObject(item.media)); - // TODO(Internal b/118431961): Add attachment management. - - JSONArray drmSchemesAsJson = new JSONArray(); - for (MediaItem.DrmScheme drmScheme : item.drmSchemes) { - JSONObject drmSchemeAsJson = new JSONObject(); - drmSchemeAsJson.put(KEY_UUID, drmScheme.uuid); - if (drmScheme.licenseServer != null) { - drmSchemeAsJson.put(KEY_LICENSE_SERVER, uriBundleAsJsonObject(drmScheme.licenseServer)); - } - drmSchemesAsJson.put(drmSchemeAsJson); - } - itemAsJson.put(KEY_DRM_SCHEMES, drmSchemesAsJson); - maybePutValue(itemAsJson, KEY_START_POSITION_US, item.startPositionUs, C.TIME_UNSET); - maybePutValue(itemAsJson, KEY_END_POSITION_US, item.endPositionUs, C.TIME_UNSET); - itemAsJson.put(KEY_MIME_TYPE, item.mimeType); - return itemAsJson; - } - - /** Returns a {@link JSONObject JSON object} containing the arguments of the message. */ - protected abstract JSONObject getArgumentsAsJsonObject() throws JSONException; - - /** Returns a JSON representation of the given {@link UriBundle}. */ - protected static JSONObject uriBundleAsJsonObject(UriBundle uriBundle) throws JSONException { - return new JSONObject() - .put(KEY_URI, uriBundle.uri) - .put(KEY_REQUEST_HEADERS, new JSONObject(uriBundle.requestHeaders)); - } - - private static JSONArray getShuffleOrderAsJson(ShuffleOrder shuffleOrder) { - JSONArray shuffleOrderJson = new JSONArray(); - int index = shuffleOrder.getFirstIndex(); - while (index != C.INDEX_UNSET) { - shuffleOrderJson.put(index); - index = shuffleOrder.getNextIndex(index); - } - return shuffleOrderJson; - } - - private static void maybePutValue(JSONObject target, String key, long value, long unsetValue) - throws JSONException { - if (value != unsetValue) { - target.put(key, value); - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java deleted file mode 100644 index 56b5d3cc8c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java +++ /dev/null @@ -1,40 +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; - -import android.content.Context; -import androidx.annotation.Nullable; -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.List; - -/** Cast options provider to target ExoPlayer's custom receiver app. */ -public final class ExoCastOptionsProvider implements OptionsProvider { - - public static final String RECEIVER_ID = "365DCC88"; - - @Override - public CastOptions getCastOptions(Context context) { - return new CastOptions.Builder().setReceiverApplicationId(RECEIVER_ID).build(); - } - - @Override - @Nullable - public List getAdditionalSessionProviders(Context context) { - return null; - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java deleted file mode 100644 index e24970ba0d..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java +++ /dev/null @@ -1,958 +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; - -import android.os.Looper; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.BasePlayer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.IllegalSeekPositionException; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.AddItems; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.MoveItem; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.RemoveItems; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetRepeatMode; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetShuffleModeEnabled; -import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetTrackSelectionParameters; -import com.google.android.exoplayer2.ext.cast.ExoCastTimeline.PeriodUid; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Plays media in a Cast receiver app that implements the ExoCast message protocol. - * - *

        The ExoCast communication protocol consists in exchanging serialized {@link ExoCastMessage - * ExoCastMessages} and {@link ReceiverAppStateUpdate receiver app state updates}. - * - *

        All methods in this class must be invoked on the main thread. Operations that change the state - * of the receiver app are masked locally as if their effect was immediate in the receiver app. - * - *

        Methods that change the state of the player must only be invoked when a session is available, - * according to {@link CastSessionManager#isCastSessionAvailable()}. - */ -public final class ExoCastPlayer extends BasePlayer { - - private static final String TAG = "ExoCastPlayer"; - - private static final int RENDERER_COUNT = 4; - private static final int RENDERER_INDEX_VIDEO = 0; - private static final int RENDERER_INDEX_AUDIO = 1; - private static final int RENDERER_INDEX_TEXT = 2; - private static final int RENDERER_INDEX_METADATA = 3; - - private final Clock clock; - private final CastSessionManager castSessionManager; - private final CopyOnWriteArrayList listeners; - private final ArrayList notificationsBatch; - private final ArrayDeque ongoingNotificationsTasks; - private final Timeline.Period scratchPeriod; - @Nullable private SessionAvailabilityListener sessionAvailabilityListener; - - // Player state. - - private final List mediaItems; - private final StateHolder currentTimeline; - private ShuffleOrder currentShuffleOrder; - - private final StateHolder playbackState; - private final StateHolder playWhenReady; - private final StateHolder repeatMode; - private final StateHolder shuffleModeEnabled; - private final StateHolder isLoading; - private final StateHolder playbackParameters; - private final StateHolder trackselectionParameters; - private final StateHolder currentTrackGroups; - private final StateHolder currentTrackSelections; - private final StateHolder<@NullableType Object> currentManifest; - private final StateHolder<@NullableType PeriodUid> currentPeriodUid; - private final StateHolder playbackPositionMs; - private final HashMap currentMediaItemInfoMap; - private long lastPlaybackPositionChangeTimeMs; - @Nullable private ExoPlaybackException playbackError; - - /** - * Creates an instance using the system clock for calculating time deltas. - * - * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. - */ - public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory) { - this(castSessionManagerFactory, Clock.DEFAULT); - } - - /** - * Creates an instance using a custom {@link Clock} implementation. - * - * @param castSessionManagerFactory Factory to create the {@link CastSessionManager}. - * @param clock The clock to use for time delta calculations. - */ - public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory, Clock clock) { - this.clock = clock; - castSessionManager = castSessionManagerFactory.create(new SessionManagerStateListener()); - listeners = new CopyOnWriteArrayList<>(); - notificationsBatch = new ArrayList<>(); - ongoingNotificationsTasks = new ArrayDeque<>(); - scratchPeriod = new Timeline.Period(); - mediaItems = new ArrayList<>(); - currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ mediaItems.size()); - playbackState = new StateHolder<>(STATE_IDLE); - playWhenReady = new StateHolder<>(false); - repeatMode = new StateHolder<>(REPEAT_MODE_OFF); - shuffleModeEnabled = new StateHolder<>(false); - isLoading = new StateHolder<>(false); - playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); - trackselectionParameters = new StateHolder<>(TrackSelectionParameters.DEFAULT); - currentTrackGroups = new StateHolder<>(TrackGroupArray.EMPTY); - currentTrackSelections = new StateHolder<>(new TrackSelectionArray(null, null, null, null)); - currentManifest = new StateHolder<>(null); - currentTimeline = new StateHolder<>(ExoCastTimeline.EMPTY); - playbackPositionMs = new StateHolder<>(0L); - currentPeriodUid = new StateHolder<>(null); - currentMediaItemInfoMap = new HashMap<>(); - castSessionManager.start(); - } - - /** Returns whether a Cast session is available. */ - public boolean isCastSessionAvailable() { - return castSessionManager.isCastSessionAvailable(); - } - - /** - * Sets a listener for updates on the Cast session availability. - * - * @param listener The {@link SessionAvailabilityListener}. - */ - public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { - sessionAvailabilityListener = listener; - } - - /** - * Prepares the player for playback. - * - *

        Sends a preparation message to the receiver. If the player is in {@link #STATE_IDLE}, - * updates the timeline with the media queue contents. - */ - public void prepare() { - long sequence = castSessionManager.send(new ExoCastMessage.Prepare()); - if (playbackState.value == STATE_IDLE) { - playbackState.sequence = sequence; - setPlaybackStateInternal(mediaItems.isEmpty() ? STATE_ENDED : STATE_BUFFERING); - if (!currentTimeline.value.representsMediaQueue( - mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { - updateTimelineInternal(TIMELINE_CHANGE_REASON_PREPARED); - } - } - flushNotifications(); - } - - /** - * Returns the item at the given index. - * - * @param index The index of the item to retrieve. - * @return The item at the given index. - */ - public MediaItem getQueueItem(int index) { - return mediaItems.get(index); - } - - /** - * Equivalent to {@link #addItemsToQueue(int, MediaItem...) addItemsToQueue(C.INDEX_UNSET, - * items)}. - */ - public void addItemsToQueue(MediaItem... items) { - addItemsToQueue(C.INDEX_UNSET, 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. - * - *

        This method discards {@code items} with a uuid that already appears in the media queue. This - * method does nothing if {@code items} contains no new items. - * - * @param optionalIndex The index at which {@code items} will be inserted. If {@link - * C#INDEX_UNSET} is passed, the items are appended to the media queue. - * @param items The sequence of items to append. {@code items} must not contain items with - * matching uuids. - * @throws IllegalArgumentException If two or more elements in {@code items} contain matching - * uuids. - */ - public void addItemsToQueue(int optionalIndex, MediaItem... items) { - // Filter out items whose uuid already appears in the queue. - ArrayList itemsToAdd = new ArrayList<>(); - HashSet addedUuids = new HashSet<>(); - for (MediaItem item : items) { - Assertions.checkArgument( - addedUuids.add(item.uuid), "Added items must contain distinct uuids"); - if (playbackState.value == STATE_IDLE - || currentTimeline.value.getWindowIndexFromUuid(item.uuid) == C.INDEX_UNSET) { - // Prevent adding items that exist in the timeline. If the player is not yet prepared, - // ignore this check, since the timeline may not reflect the current media queue. - // Preparation will filter any duplicates. - itemsToAdd.add(item); - } - } - if (itemsToAdd.isEmpty()) { - return; - } - - int normalizedIndex; - if (optionalIndex != C.INDEX_UNSET) { - normalizedIndex = optionalIndex; - mediaItems.addAll(optionalIndex, itemsToAdd); - } else { - normalizedIndex = mediaItems.size(); - mediaItems.addAll(itemsToAdd); - } - currentShuffleOrder = currentShuffleOrder.cloneAndInsert(normalizedIndex, itemsToAdd.size()); - long sequence = - castSessionManager.send(new AddItems(optionalIndex, itemsToAdd, currentShuffleOrder)); - if (playbackState.value != STATE_IDLE) { - currentTimeline.sequence = sequence; - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - } - flushNotifications(); - } - - /** - * Moves an existing item within the queue. - * - *

        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 index The index of the item to move. - * @param newIndex The index at which the item will be placed after this operation. - */ - public void moveItemInQueue(int index, int newIndex) { - MediaItem movedItem = mediaItems.remove(index); - mediaItems.add(newIndex, movedItem); - currentShuffleOrder = - currentShuffleOrder - .cloneAndRemove(index, index + 1) - .cloneAndInsert(newIndex, /* insertionCount= */ 1); - long sequence = - castSessionManager.send(new MoveItem(movedItem.uuid, newIndex, currentShuffleOrder)); - if (playbackState.value != STATE_IDLE) { - currentTimeline.sequence = sequence; - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - } - flushNotifications(); - } - - /** - * Removes an item from the queue. - * - * @param index The index of the item to remove from the queue. - */ - public void removeItemFromQueue(int index) { - removeRangeFromQueue(index, index + 1); - } - - /** - * Removes a range of items from the queue. - * - *

        If the currently-playing item is removed, the playback position moves to the item following - * the removed range. If no item follows the removed range, the position is set to the last item - * in the queue and the player state transitions to {@link #STATE_ENDED}. Does nothing if an empty - * range ({@code from == exclusiveTo}) is passed. - * - * @param indexFrom The inclusive index at which the range to remove starts. - * @param indexExclusiveTo The exclusive index at which the range to remove ends. - */ - public void removeRangeFromQueue(int indexFrom, int indexExclusiveTo) { - UUID[] uuidsToRemove = new UUID[indexExclusiveTo - indexFrom]; - for (int i = 0; i < uuidsToRemove.length; i++) { - uuidsToRemove[i] = mediaItems.get(i + indexFrom).uuid; - } - - int windowIndexBeforeRemoval = getCurrentWindowIndex(); - boolean currentItemWasRemoved = - windowIndexBeforeRemoval >= indexFrom && windowIndexBeforeRemoval < indexExclusiveTo; - boolean shouldTransitionToEnded = - currentItemWasRemoved && indexExclusiveTo == mediaItems.size(); - - Util.removeRange(mediaItems, indexFrom, indexExclusiveTo); - long sequence = castSessionManager.send(new RemoveItems(Arrays.asList(uuidsToRemove))); - currentShuffleOrder = currentShuffleOrder.cloneAndRemove(indexFrom, indexExclusiveTo); - - if (playbackState.value != STATE_IDLE) { - currentTimeline.sequence = sequence; - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - if (currentItemWasRemoved) { - int newWindowIndex = Math.max(0, indexFrom - (shouldTransitionToEnded ? 1 : 0)); - PeriodUid periodUid = - currentTimeline.value.isEmpty() - ? null - : (PeriodUid) - currentTimeline.value.getPeriodPosition( - window, - scratchPeriod, - newWindowIndex, - /* windowPositionUs= */ C.TIME_UNSET) - .first; - currentPeriodUid.sequence = sequence; - playbackPositionMs.sequence = sequence; - setPlaybackPositionInternal( - periodUid, - /* positionMs= */ C.TIME_UNSET, - /* discontinuityReason= */ DISCONTINUITY_REASON_SEEK); - } - playbackState.sequence = sequence; - setPlaybackStateInternal(shouldTransitionToEnded ? STATE_ENDED : STATE_BUFFERING); - } - flushNotifications(); - } - - /** Removes all items in the queue. */ - public void clearQueue() { - removeRangeFromQueue(0, getQueueSize()); - } - - /** Returns the number of items in this queue. */ - public int getQueueSize() { - return mediaItems.size(); - } - - // Track selection. - - /** - * Provides a set of constrains for the receiver app to execute track selection. - * - *

        {@link TrackSelectionParameters} passed to this method may be {@link - * TrackSelectionParameters#buildUpon() built upon} by this player as a result of a remote - * operation, which means {@link TrackSelectionParameters} obtained from {@link - * #getTrackSelectionParameters()} may have field differences with {@code parameters} passed to - * this method. However, only fields modified remotely will present differences. Other fields will - * remain unchanged. - */ - public void setTrackSelectionParameters(TrackSelectionParameters trackselectionParameters) { - this.trackselectionParameters.value = trackselectionParameters; - this.trackselectionParameters.sequence = - castSessionManager.send(new SetTrackSelectionParameters(trackselectionParameters)); - } - - /** - * Retrieves the current {@link TrackSelectionParameters}. See {@link - * #setTrackSelectionParameters(TrackSelectionParameters)}. - */ - public TrackSelectionParameters getTrackSelectionParameters() { - return trackselectionParameters.value; - } - - // Player Implementation. - - @Override - @Nullable - public AudioComponent getAudioComponent() { - // TODO: Implement volume controls using the audio component. - return null; - } - - @Override - @Nullable - public VideoComponent getVideoComponent() { - return null; - } - - @Override - @Nullable - public TextComponent getTextComponent() { - return null; - } - - @Override - @Nullable - public MetadataComponent getMetadataComponent() { - return null; - } - - @Override - public Looper getApplicationLooper() { - return Looper.getMainLooper(); - } - - @Override - public void addListener(EventListener listener) { - listeners.addIfAbsent(new ListenerHolder(listener)); - } - - @Override - public void removeListener(EventListener listener) { - for (ListenerHolder listenerHolder : listeners) { - if (listenerHolder.listener.equals(listener)) { - listenerHolder.release(); - listeners.remove(listenerHolder); - } - } - } - - @Override - @Player.State - public int getPlaybackState() { - return playbackState.value; - } - - @Nullable - @Override - public ExoPlaybackException getPlaybackError() { - return playbackError; - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - this.playWhenReady.sequence = - castSessionManager.send(new ExoCastMessage.SetPlayWhenReady(playWhenReady)); - // Take a snapshot of the playback position before pausing to ensure future calculations are - // correct. - setPlaybackPositionInternal( - currentPeriodUid.value, getCurrentPosition(), /* discontinuityReason= */ null); - setPlayWhenReadyInternal(playWhenReady); - flushNotifications(); - } - - @Override - public boolean getPlayWhenReady() { - return playWhenReady.value; - } - - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - this.repeatMode.sequence = castSessionManager.send(new SetRepeatMode(repeatMode)); - setRepeatModeInternal(repeatMode); - flushNotifications(); - } - - @Override - @RepeatMode - public int getRepeatMode() { - return repeatMode.value; - } - - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - this.shuffleModeEnabled.sequence = - castSessionManager.send(new SetShuffleModeEnabled(shuffleModeEnabled)); - setShuffleModeEnabledInternal(shuffleModeEnabled); - flushNotifications(); - } - - @Override - public boolean getShuffleModeEnabled() { - return shuffleModeEnabled.value; - } - - @Override - public boolean isLoading() { - return isLoading.value; - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - if (mediaItems.isEmpty()) { - // TODO: Handle seeking in empty timeline. - setPlaybackPositionInternal(/* periodUid= */ null, 0, DISCONTINUITY_REASON_SEEK); - return; - } else if (windowIndex >= mediaItems.size()) { - throw new IllegalSeekPositionException(currentTimeline.value, windowIndex, positionMs); - } - long sequence = - castSessionManager.send( - new ExoCastMessage.SeekTo(mediaItems.get(windowIndex).uuid, positionMs)); - - currentPeriodUid.sequence = sequence; - playbackPositionMs.sequence = sequence; - - PeriodUid periodUid = - (PeriodUid) - currentTimeline.value.getPeriodPosition( - window, scratchPeriod, windowIndex, C.msToUs(positionMs)) - .first; - setPlaybackPositionInternal(periodUid, positionMs, DISCONTINUITY_REASON_SEEK); - if (playbackState.value != STATE_IDLE) { - playbackState.sequence = sequence; - setPlaybackStateInternal(STATE_BUFFERING); - } - flushNotifications(); - } - - @Override - public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - playbackParameters = - playbackParameters != null ? playbackParameters : PlaybackParameters.DEFAULT; - this.playbackParameters.value = playbackParameters; - this.playbackParameters.sequence = - castSessionManager.send(new ExoCastMessage.SetPlaybackParameters(playbackParameters)); - this.playbackParameters.value = playbackParameters; - // Note: This method, unlike others, does not immediately notify the change. See the Player - // interface for more information. - } - - @Override - public PlaybackParameters getPlaybackParameters() { - return playbackParameters.value; - } - - @Override - public void stop(boolean reset) { - long sequence = castSessionManager.send(new ExoCastMessage.Stop(reset)); - playbackState.sequence = sequence; - setPlaybackStateInternal(STATE_IDLE); - if (reset) { - currentTimeline.sequence = sequence; - mediaItems.clear(); - currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length =*/ 0); - setPlaybackPositionInternal( - /* periodUid= */ null, /* positionMs= */ 0, DISCONTINUITY_REASON_INTERNAL); - updateTimelineInternal(TIMELINE_CHANGE_REASON_RESET); - } - flushNotifications(); - } - - @Override - public void release() { - setSessionAvailabilityListener(null); - castSessionManager.stopTrackingSession(); - flushNotifications(); - } - - @Override - public int getRendererCount() { - return RENDERER_COUNT; - } - - @Override - public int getRendererType(int index) { - switch (index) { - case RENDERER_INDEX_VIDEO: - return C.TRACK_TYPE_VIDEO; - case RENDERER_INDEX_AUDIO: - return C.TRACK_TYPE_AUDIO; - case RENDERER_INDEX_TEXT: - return C.TRACK_TYPE_TEXT; - case RENDERER_INDEX_METADATA: - return C.TRACK_TYPE_METADATA; - default: - throw new IndexOutOfBoundsException(); - } - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. - return currentTrackGroups.value; - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. - return currentTrackSelections.value; - } - - @Override - @Nullable - public Object getCurrentManifest() { - // TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap. - return currentManifest.value; - } - - @Override - public Timeline getCurrentTimeline() { - return currentTimeline.value; - } - - @Override - public int getCurrentPeriodIndex() { - int periodIndex = - currentPeriodUid.value == null - ? C.INDEX_UNSET - : currentTimeline.value.getIndexOfPeriod(currentPeriodUid.value); - return periodIndex != C.INDEX_UNSET ? periodIndex : 0; - } - - @Override - public int getCurrentWindowIndex() { - int windowIndex = - currentPeriodUid.value == null - ? C.INDEX_UNSET - : currentTimeline.value.getWindowIndexContainingPeriod(currentPeriodUid.value); - return windowIndex != C.INDEX_UNSET ? windowIndex : 0; - } - - @Override - public long getDuration() { - return getContentDuration(); - } - - @Override - public long getCurrentPosition() { - return playbackPositionMs.value - + (getPlaybackState() == STATE_READY && getPlayWhenReady() - ? projectPlaybackTimeElapsedMs() - : 0L); - } - - @Override - public long getBufferedPosition() { - return getCurrentPosition(); - } - - @Override - public long getTotalBufferedDuration() { - return 0; - } - - @Override - public boolean isPlayingAd() { - // TODO (Internal b/119293631): Add support for ads. - return false; - } - - @Override - public int getCurrentAdGroupIndex() { - return C.INDEX_UNSET; - } - - @Override - public int getCurrentAdIndexInAdGroup() { - return C.INDEX_UNSET; - } - - @Override - public long getContentPosition() { - return getCurrentPosition(); - } - - @Override - public long getContentBufferedPosition() { - return getCurrentPosition(); - } - - // Local state modifications. - - private void setPlayWhenReadyInternal(boolean playWhenReady) { - if (this.playWhenReady.value != playWhenReady) { - this.playWhenReady.value = playWhenReady; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlayerStateChanged(playWhenReady, playbackState.value))); - } - } - - private void setPlaybackStateInternal(int playbackState) { - if (this.playbackState.value != playbackState) { - if (this.playbackState.value == STATE_IDLE) { - // We are transitioning out of STATE_IDLE. We clear any errors. - setPlaybackErrorInternal(null); - } - this.playbackState.value = playbackState; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlayerStateChanged(playWhenReady.value, playbackState))); - } - } - - private void setRepeatModeInternal(int repeatMode) { - if (this.repeatMode.value != repeatMode) { - this.repeatMode.value = repeatMode; - notificationsBatch.add( - new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode))); - } - } - - private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) { - if (this.shuffleModeEnabled.value != shuffleModeEnabled) { - this.shuffleModeEnabled.value = shuffleModeEnabled; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled))); - } - } - - private void setIsLoadingInternal(boolean isLoading) { - if (this.isLoading.value != isLoading) { - this.isLoading.value = isLoading; - notificationsBatch.add( - new ListenerNotificationTask(listener -> listener.onLoadingChanged(isLoading))); - } - } - - private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - if (!this.playbackParameters.value.equals(playbackParameters)) { - this.playbackParameters.value = playbackParameters; - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlaybackParametersChanged(playbackParameters))); - } - } - - private void setPlaybackErrorInternal(@Nullable String errorMessage) { - if (errorMessage != null) { - playbackError = ExoPlaybackException.createForRemote(errorMessage); - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPlayerError(Assertions.checkNotNull(playbackError)))); - } else { - playbackError = null; - } - } - - private void setPlaybackPositionInternal( - @Nullable PeriodUid periodUid, long positionMs, @Nullable Integer discontinuityReason) { - currentPeriodUid.value = periodUid; - if (periodUid == null) { - positionMs = 0L; - } else if (positionMs == C.TIME_UNSET) { - int windowIndex = currentTimeline.value.getWindowIndexContainingPeriod(periodUid); - if (windowIndex == C.INDEX_UNSET) { - positionMs = 0; - } else { - positionMs = - C.usToMs( - currentTimeline.value.getWindow(windowIndex, window, /* setTag= */ false) - .defaultPositionUs); - } - } - playbackPositionMs.value = positionMs; - lastPlaybackPositionChangeTimeMs = clock.elapsedRealtime(); - if (discontinuityReason != null) { - notificationsBatch.add( - new ListenerNotificationTask( - listener -> listener.onPositionDiscontinuity(discontinuityReason))); - } - } - - // Internal methods. - - private void updateTimelineInternal(@TimelineChangeReason int changeReason) { - currentTimeline.value = - ExoCastTimeline.createTimelineFor(mediaItems, currentMediaItemInfoMap, currentShuffleOrder); - removeStaleMediaItemInfo(); - notificationsBatch.add( - new ListenerNotificationTask( - listener -> - listener.onTimelineChanged( - currentTimeline.value, /* manifest= */ null, changeReason))); - } - - private long projectPlaybackTimeElapsedMs() { - return (long) - ((clock.elapsedRealtime() - lastPlaybackPositionChangeTimeMs) - * playbackParameters.value.speed); - } - - 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(); - } - } - - /** - * Updates the current media item information by including any extra entries received from the - * receiver app. - * - * @param mediaItemsInformation A map of media item information received from the receiver app. - */ - private void updateMediaItemsInfo(Map mediaItemsInformation) { - for (Map.Entry entry : mediaItemsInformation.entrySet()) { - MediaItemInfo currentInfoForEntry = currentMediaItemInfoMap.get(entry.getKey()); - boolean shouldPutEntry = - currentInfoForEntry == null || !currentInfoForEntry.equals(entry.getValue()); - if (shouldPutEntry) { - currentMediaItemInfoMap.put(entry.getKey(), entry.getValue()); - } - } - } - - /** - * Removes stale media info entries. An entry is considered stale when the corresponding media - * item is not present in the current media queue. - */ - private void removeStaleMediaItemInfo() { - for (Iterator iterator = currentMediaItemInfoMap.keySet().iterator(); - iterator.hasNext(); ) { - UUID uuid = iterator.next(); - if (currentTimeline.value.getWindowIndexFromUuid(uuid) == C.INDEX_UNSET) { - iterator.remove(); - } - } - } - - // Internal classes. - - private class SessionManagerStateListener implements CastSessionManager.StateListener { - - @Override - public void onCastSessionAvailable() { - if (sessionAvailabilityListener != null) { - sessionAvailabilityListener.onCastSessionAvailable(); - } - } - - @Override - public void onCastSessionUnavailable() { - if (sessionAvailabilityListener != null) { - sessionAvailabilityListener.onCastSessionUnavailable(); - } - } - - @Override - public void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate) { - long sequence = stateUpdate.sequenceNumber; - - if (stateUpdate.errorMessage != null) { - setPlaybackErrorInternal(stateUpdate.errorMessage); - } - - if (sequence >= playbackState.sequence && stateUpdate.playbackState != null) { - setPlaybackStateInternal(stateUpdate.playbackState); - } - - if (sequence >= currentTimeline.sequence) { - if (stateUpdate.items != null) { - mediaItems.clear(); - mediaItems.addAll(stateUpdate.items); - } - - currentShuffleOrder = - stateUpdate.shuffleOrder != null - ? new ShuffleOrder.DefaultShuffleOrder( - Util.toArray(stateUpdate.shuffleOrder), clock.elapsedRealtime()) - : currentShuffleOrder; - updateMediaItemsInfo(stateUpdate.mediaItemsInformation); - - if (playbackState.value != STATE_IDLE - && !currentTimeline.value.representsMediaQueue( - mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) { - updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC); - } - } - - if (sequence >= currentPeriodUid.sequence - && stateUpdate.currentPlayingItemUuid != null - && stateUpdate.currentPlaybackPositionMs != null) { - PeriodUid periodUid; - if (stateUpdate.currentPlayingPeriodId == null) { - int windowIndex = - currentTimeline.value.getWindowIndexFromUuid(stateUpdate.currentPlayingItemUuid); - periodUid = - (PeriodUid) - currentTimeline.value.getPeriodPosition( - window, - scratchPeriod, - windowIndex, - C.msToUs(stateUpdate.currentPlaybackPositionMs)) - .first; - } else { - periodUid = - ExoCastTimeline.createPeriodUid( - stateUpdate.currentPlayingItemUuid, stateUpdate.currentPlayingPeriodId); - } - setPlaybackPositionInternal( - periodUid, stateUpdate.currentPlaybackPositionMs, stateUpdate.discontinuityReason); - } - - if (sequence >= isLoading.sequence && stateUpdate.isLoading != null) { - setIsLoadingInternal(stateUpdate.isLoading); - } - - if (sequence >= playWhenReady.sequence && stateUpdate.playWhenReady != null) { - setPlayWhenReadyInternal(stateUpdate.playWhenReady); - } - - if (sequence >= shuffleModeEnabled.sequence && stateUpdate.shuffleModeEnabled != null) { - setShuffleModeEnabledInternal(stateUpdate.shuffleModeEnabled); - } - - if (sequence >= repeatMode.sequence && stateUpdate.repeatMode != null) { - setRepeatModeInternal(stateUpdate.repeatMode); - } - - if (sequence >= playbackParameters.sequence && stateUpdate.playbackParameters != null) { - setPlaybackParametersInternal(stateUpdate.playbackParameters); - } - - TrackSelectionParameters parameters = stateUpdate.trackSelectionParameters; - if (sequence >= trackselectionParameters.sequence && parameters != null) { - trackselectionParameters.value = - trackselectionParameters - .value - .buildUpon() - .setDisabledTextTrackSelectionFlags(parameters.disabledTextTrackSelectionFlags) - .setPreferredAudioLanguage(parameters.preferredAudioLanguage) - .setPreferredTextLanguage(parameters.preferredTextLanguage) - .setSelectUndeterminedTextLanguage(parameters.selectUndeterminedTextLanguage) - .build(); - } - - flushNotifications(); - } - } - - private static final class StateHolder { - - public T value; - public long sequence; - - public StateHolder(T initialValue) { - value = initialValue; - sequence = CastSessionManager.SEQUENCE_NUMBER_UNSET; - } - } - - 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/ExoCastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java deleted file mode 100644 index 115536ac4c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java +++ /dev/null @@ -1,342 +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; - -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.source.ShuffleOrder; -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; - -/** - * A {@link Timeline} for Cast receiver app media queues. - * - *

        Each {@link MediaItem} in the timeline is exposed as a window. Unprepared media items are - * exposed as an unset-duration {@link Window}, with a single unset-duration {@link Period}. - */ -/* package */ final class ExoCastTimeline extends Timeline { - - /** Opaque object that uniquely identifies a period across timeline changes. */ - public interface PeriodUid {} - - /** A timeline for an empty media queue. */ - public static final ExoCastTimeline EMPTY = - createTimelineFor( - Collections.emptyList(), Collections.emptyMap(), new ShuffleOrder.DefaultShuffleOrder(0)); - - /** - * Creates {@link PeriodUid} from the given arguments. - * - * @param itemUuid The UUID that identifies the item. - * @param periodId The id of the period for which the unique identifier is required. - * @return An opaque unique identifier for a period. - */ - public static PeriodUid createPeriodUid(UUID itemUuid, Object periodId) { - return new PeriodUidImpl(itemUuid, periodId); - } - - /** - * Returns a new timeline representing the given media queue information. - * - * @param mediaItems The media items conforming the timeline. - * @param mediaItemInfoMap Maps {@link MediaItem media items} in {@code mediaItems} to a {@link - * MediaItemInfo} through their {@link MediaItem#uuid}. Media items may not have a {@link - * MediaItemInfo} mapped to them. - * @param shuffleOrder The {@link ShuffleOrder} of the timeline. {@link ShuffleOrder#getLength()} - * must be equal to {@code mediaItems.size()}. - * @return A new timeline representing the given media queue information. - */ - public static ExoCastTimeline createTimelineFor( - List mediaItems, - Map mediaItemInfoMap, - ShuffleOrder shuffleOrder) { - Assertions.checkArgument(mediaItems.size() == shuffleOrder.getLength()); - int[] accumulativePeriodCount = new int[mediaItems.size()]; - int periodCount = 0; - for (int i = 0; i < accumulativePeriodCount.length; i++) { - periodCount += getInfoOrEmpty(mediaItemInfoMap, mediaItems.get(i).uuid).periods.size(); - accumulativePeriodCount[i] = periodCount; - } - HashMap uuidToIndex = new HashMap<>(); - for (int i = 0; i < mediaItems.size(); i++) { - uuidToIndex.put(mediaItems.get(i).uuid, i); - } - return new ExoCastTimeline( - Collections.unmodifiableList(new ArrayList<>(mediaItems)), - Collections.unmodifiableMap(new HashMap<>(mediaItemInfoMap)), - Collections.unmodifiableMap(new HashMap<>(uuidToIndex)), - shuffleOrder, - accumulativePeriodCount); - } - - // Timeline backing information. - private final List mediaItems; - private final Map mediaItemInfoMap; - private final ShuffleOrder shuffleOrder; - - // Precomputed for quick access. - private final Map uuidToIndex; - private final int[] accumulativePeriodCount; - - private ExoCastTimeline( - List mediaItems, - Map mediaItemInfoMap, - Map uuidToIndex, - ShuffleOrder shuffleOrder, - int[] accumulativePeriodCount) { - this.mediaItems = mediaItems; - this.mediaItemInfoMap = mediaItemInfoMap; - this.uuidToIndex = uuidToIndex; - this.shuffleOrder = shuffleOrder; - this.accumulativePeriodCount = accumulativePeriodCount; - } - - /** - * Returns whether the given media queue information would produce a timeline equivalent to this - * one. - * - * @see ExoCastTimeline#createTimelineFor(List, Map, ShuffleOrder) - */ - public boolean representsMediaQueue( - List mediaItems, - Map mediaItemInfoMap, - ShuffleOrder shuffleOrder) { - if (this.shuffleOrder.getLength() != shuffleOrder.getLength()) { - return false; - } - - int index = shuffleOrder.getFirstIndex(); - if (this.shuffleOrder.getFirstIndex() != index) { - return false; - } - while (index != C.INDEX_UNSET) { - int nextIndex = shuffleOrder.getNextIndex(index); - if (nextIndex != this.shuffleOrder.getNextIndex(index)) { - return false; - } - index = nextIndex; - } - - if (mediaItems.size() != this.mediaItems.size()) { - return false; - } - for (int i = 0; i < mediaItems.size(); i++) { - UUID uuid = mediaItems.get(i).uuid; - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); - if (!uuid.equals(this.mediaItems.get(i).uuid) - || !mediaItemInfo.equals(getInfoOrEmpty(this.mediaItemInfoMap, uuid))) { - return false; - } - } - return true; - } - - /** - * Returns the index of the window that contains the period identified by the given {@code - * periodUid} or {@link C#INDEX_UNSET} if this timeline does not contain any period with the given - * {@code periodUid}. - */ - public int getWindowIndexContainingPeriod(PeriodUid periodUid) { - if (!(periodUid instanceof PeriodUidImpl)) { - return C.INDEX_UNSET; - } - return getWindowIndexFromUuid(((PeriodUidImpl) periodUid).itemUuid); - } - - /** - * Returns the index of the window that represents the media item with the given {@code uuid} or - * {@link C#INDEX_UNSET} if no item in this timeline has the given {@code uuid}. - */ - public int getWindowIndexFromUuid(UUID uuid) { - Integer index = uuidToIndex.get(uuid); - return index != null ? index : C.INDEX_UNSET; - } - - // Timeline implementation. - - @Override - public int getWindowCount() { - return mediaItems.size(); - } - - @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - MediaItem mediaItem = mediaItems.get(windowIndex); - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, mediaItem.uuid); - return window.set( - /* tag= */ setTag ? mediaItem.attachment : null, - /* presentationStartTimeMs= */ C.TIME_UNSET, - /* windowStartTimeMs= */ C.TIME_UNSET, - /* isSeekable= */ mediaItemInfo.isSeekable, - /* isDynamic= */ mediaItemInfo.isDynamic, - /* defaultPositionUs= */ mediaItemInfo.defaultStartPositionUs, - /* durationUs= */ mediaItemInfo.windowDurationUs, - /* firstPeriodIndex= */ windowIndex == 0 ? 0 : accumulativePeriodCount[windowIndex - 1], - /* lastPeriodIndex= */ accumulativePeriodCount[windowIndex] - 1, - mediaItemInfo.positionInFirstPeriodUs); - } - - @Override - public int getPeriodCount() { - return mediaItems.isEmpty() ? 0 : accumulativePeriodCount[accumulativePeriodCount.length - 1]; - } - - @Override - public Period getPeriodByUid(Object periodUidObject, Period period) { - return getPeriodInternal((PeriodUidImpl) periodUidObject, period, /* setIds= */ true); - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return getPeriodInternal((PeriodUidImpl) getUidOfPeriod(periodIndex), period, setIds); - } - - @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof PeriodUidImpl)) { - return C.INDEX_UNSET; - } - PeriodUidImpl periodUid = (PeriodUidImpl) uid; - UUID uuid = periodUid.itemUuid; - Integer itemIndex = uuidToIndex.get(uuid); - if (itemIndex == null) { - return C.INDEX_UNSET; - } - int indexOfPeriodInItem = - getInfoOrEmpty(mediaItemInfoMap, uuid).getIndexOfPeriod(periodUid.periodId); - if (indexOfPeriodInItem == C.INDEX_UNSET) { - return C.INDEX_UNSET; - } - return indexOfPeriodInItem + (itemIndex == 0 ? 0 : accumulativePeriodCount[itemIndex - 1]); - } - - @Override - public PeriodUid getUidOfPeriod(int periodIndex) { - int mediaItemIndex = getMediaItemIndexForPeriodIndex(periodIndex); - int periodIndexInMediaItem = - periodIndex - (mediaItemIndex > 0 ? accumulativePeriodCount[mediaItemIndex - 1] : 0); - UUID uuid = mediaItems.get(mediaItemIndex).uuid; - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); - return new PeriodUidImpl(uuid, mediaItemInfo.periods.get(periodIndexInMediaItem).id); - } - - @Override - public int getFirstWindowIndex(boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; - } - - @Override - public int getLastWindowIndex(boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getLastIndex() : mediaItems.size() - 1; - } - - @Override - public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { - if (repeatMode == Player.REPEAT_MODE_ONE) { - return windowIndex; - } else if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { - return repeatMode == Player.REPEAT_MODE_OFF - ? C.INDEX_UNSET - : getLastWindowIndex(shuffleModeEnabled); - } else if (shuffleModeEnabled) { - return shuffleOrder.getPreviousIndex(windowIndex); - } else { - return windowIndex - 1; - } - } - - @Override - public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { - if (repeatMode == Player.REPEAT_MODE_ONE) { - return windowIndex; - } else if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { - return repeatMode == Player.REPEAT_MODE_OFF - ? C.INDEX_UNSET - : getFirstWindowIndex(shuffleModeEnabled); - } else if (shuffleModeEnabled) { - return shuffleOrder.getNextIndex(windowIndex); - } else { - return windowIndex + 1; - } - } - - // Internal methods. - - private Period getPeriodInternal(PeriodUidImpl uid, Period period, boolean setIds) { - UUID uuid = uid.itemUuid; - int itemIndex = Assertions.checkNotNull(uuidToIndex.get(uuid)); - MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid); - MediaItemInfo.Period mediaInfoPeriod = - mediaItemInfo.periods.get(mediaItemInfo.getIndexOfPeriod(uid.periodId)); - return period.set( - setIds ? mediaInfoPeriod.id : null, - setIds ? uid : null, - /* windowIndex= */ itemIndex, - mediaInfoPeriod.durationUs, - mediaInfoPeriod.positionInWindowUs); - } - - private int getMediaItemIndexForPeriodIndex(int periodIndex) { - return Util.binarySearchCeil( - accumulativePeriodCount, periodIndex, /* inclusive= */ false, /* stayInBounds= */ false); - } - - private static MediaItemInfo getInfoOrEmpty(Map map, UUID uuid) { - MediaItemInfo info = map.get(uuid); - return info != null ? info : MediaItemInfo.EMPTY; - } - - // Internal classes. - - private static final class PeriodUidImpl implements PeriodUid { - - public final UUID itemUuid; - public final Object periodId; - - private PeriodUidImpl(UUID itemUuid, Object periodId) { - this.itemUuid = itemUuid; - this.periodId = periodId; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - PeriodUidImpl periodUid = (PeriodUidImpl) other; - return itemUuid.equals(periodUid.itemUuid) && periodId.equals(periodUid.periodId); - } - - @Override - public int hashCode() { - int result = itemUuid.hashCode(); - result = 31 * result + periodId.hashCode(); - return result; - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java deleted file mode 100644 index cb5eff4f37..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java +++ /dev/null @@ -1,160 +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; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Util; -import java.util.Collections; -import java.util.List; - -// TODO (Internal b/119293631): Add ad playback state info. -/** - * Holds dynamic information for a {@link MediaItem}. - * - *

        Holds information related to preparation for a specific {@link MediaItem}. Unprepared items - * are associated with an {@link #EMPTY} info object until prepared. - */ -public final class MediaItemInfo { - - /** Placeholder information for media items that have not yet been prepared by the player. */ - public static final MediaItemInfo EMPTY = - new MediaItemInfo( - /* windowDurationUs= */ C.TIME_UNSET, - /* defaultStartPositionUs= */ 0L, - Collections.singletonList( - new Period( - /* id= */ new Object(), - /* durationUs= */ C.TIME_UNSET, - /* positionInWindowUs= */ 0L)), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ false, - /* isDynamic= */ true); - - /** Holds the information of one of the periods of a {@link MediaItem}. */ - public static final class Period { - - /** - * The id of the period. Must be unique within the {@link MediaItem} but may match with periods - * in other items. - */ - public final Object id; - /** The duration of the period in microseconds. */ - public final long durationUs; - /** The position of this period in the window in microseconds. */ - public final long positionInWindowUs; - // TODO: Add track information. - - public Period(Object id, long durationUs, long positionInWindowUs) { - this.id = id; - this.durationUs = durationUs; - this.positionInWindowUs = positionInWindowUs; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - - Period period = (Period) other; - return durationUs == period.durationUs - && positionInWindowUs == period.positionInWindowUs - && id.equals(period.id); - } - - @Override - public int hashCode() { - int result = id.hashCode(); - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); - return result; - } - } - - /** The duration of the window in microseconds. */ - public final long windowDurationUs; - /** The default start position relative to the start of the window, in microseconds. */ - public final long defaultStartPositionUs; - /** The periods conforming the media item. */ - public final List periods; - /** The position of the window in the first period in microseconds. */ - public final long positionInFirstPeriodUs; - /** Whether it is possible to seek within the window. */ - public final boolean isSeekable; - /** Whether the window may change when the timeline is updated. */ - public final boolean isDynamic; - - public MediaItemInfo( - long windowDurationUs, - long defaultStartPositionUs, - List periods, - long positionInFirstPeriodUs, - boolean isSeekable, - boolean isDynamic) { - this.windowDurationUs = windowDurationUs; - this.defaultStartPositionUs = defaultStartPositionUs; - this.periods = Collections.unmodifiableList(periods); - this.positionInFirstPeriodUs = positionInFirstPeriodUs; - this.isSeekable = isSeekable; - this.isDynamic = isDynamic; - } - - /** - * Returns the index of the period with {@link Period#id} equal to {@code periodId}, or {@link - * C#INDEX_UNSET} if none of the periods has the given id. - */ - public int getIndexOfPeriod(Object periodId) { - for (int i = 0; i < periods.size(); i++) { - if (Util.areEqual(periods.get(i).id, periodId)) { - return i; - } - } - return C.INDEX_UNSET; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - - MediaItemInfo that = (MediaItemInfo) other; - return windowDurationUs == that.windowDurationUs - && defaultStartPositionUs == that.defaultStartPositionUs - && positionInFirstPeriodUs == that.positionInFirstPeriodUs - && isSeekable == that.isSeekable - && isDynamic == that.isDynamic - && periods.equals(that.periods); - } - - @Override - public int hashCode() { - int result = (int) (windowDurationUs ^ (windowDurationUs >>> 32)); - result = 31 * result + (int) (defaultStartPositionUs ^ (defaultStartPositionUs >>> 32)); - result = 31 * result + periods.hashCode(); - result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); - result = 31 * result + (isSeekable ? 1 : 0); - result = 31 * result + (isDynamic ? 1 : 0); - return result; - } -} 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/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java deleted file mode 100644 index c1b12428d4..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java +++ /dev/null @@ -1,633 +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.ext.cast; - -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.Player.STATE_BUFFERING; -import static com.google.android.exoplayer2.Player.STATE_ENDED; -import static com.google.android.exoplayer2.Player.STATE_IDLE; -import static com.google.android.exoplayer2.Player.STATE_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_AD_INSERTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_PERIOD_TRANSITION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_ENDED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_IDLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_READY; - -import android.net.Uri; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -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.Iterator; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** Holds a playback state update from the receiver app. */ -public final class ReceiverAppStateUpdate { - - /** Builder for {@link ReceiverAppStateUpdate}. */ - public static final class Builder { - - private final long sequenceNumber; - private @MonotonicNonNull Boolean playWhenReady; - private @MonotonicNonNull Integer playbackState; - private @MonotonicNonNull List items; - private @MonotonicNonNull Integer repeatMode; - private @MonotonicNonNull Boolean shuffleModeEnabled; - private @MonotonicNonNull Boolean isLoading; - private @MonotonicNonNull PlaybackParameters playbackParameters; - private @MonotonicNonNull TrackSelectionParameters trackSelectionParameters; - private @MonotonicNonNull String errorMessage; - private @MonotonicNonNull Integer discontinuityReason; - private @MonotonicNonNull UUID currentPlayingItemUuid; - private @MonotonicNonNull String currentPlayingPeriodId; - private @MonotonicNonNull Long currentPlaybackPositionMs; - private @MonotonicNonNull List shuffleOrder; - private Map mediaItemsInformation; - - private Builder(long sequenceNumber) { - this.sequenceNumber = sequenceNumber; - mediaItemsInformation = Collections.emptyMap(); - } - - /** See {@link ReceiverAppStateUpdate#playWhenReady}. */ - public Builder setPlayWhenReady(Boolean playWhenReady) { - this.playWhenReady = playWhenReady; - return this; - } - - /** See {@link ReceiverAppStateUpdate#playbackState}. */ - public Builder setPlaybackState(Integer playbackState) { - this.playbackState = playbackState; - return this; - } - - /** See {@link ReceiverAppStateUpdate#items}. */ - public Builder setItems(List items) { - this.items = Collections.unmodifiableList(items); - return this; - } - - /** See {@link ReceiverAppStateUpdate#repeatMode}. */ - public Builder setRepeatMode(Integer repeatMode) { - this.repeatMode = repeatMode; - return this; - } - - /** See {@link ReceiverAppStateUpdate#shuffleModeEnabled}. */ - public Builder setShuffleModeEnabled(Boolean shuffleModeEnabled) { - this.shuffleModeEnabled = shuffleModeEnabled; - return this; - } - - /** See {@link ReceiverAppStateUpdate#isLoading}. */ - public Builder setIsLoading(Boolean isLoading) { - this.isLoading = isLoading; - return this; - } - - /** See {@link ReceiverAppStateUpdate#playbackParameters}. */ - public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { - this.playbackParameters = playbackParameters; - return this; - } - - /** See {@link ReceiverAppStateUpdate#trackSelectionParameters} */ - public Builder setTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) { - this.trackSelectionParameters = trackSelectionParameters; - return this; - } - - /** See {@link ReceiverAppStateUpdate#errorMessage}. */ - public Builder setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - return this; - } - - /** See {@link ReceiverAppStateUpdate#discontinuityReason}. */ - public Builder setDiscontinuityReason(Integer discontinuityReason) { - this.discontinuityReason = discontinuityReason; - return this; - } - - /** - * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link - * ReceiverAppStateUpdate#currentPlaybackPositionMs}. - */ - public Builder setPlaybackPosition( - UUID currentPlayingItemUuid, - String currentPlayingPeriodId, - Long currentPlaybackPositionMs) { - this.currentPlayingItemUuid = currentPlayingItemUuid; - this.currentPlayingPeriodId = currentPlayingPeriodId; - this.currentPlaybackPositionMs = currentPlaybackPositionMs; - return this; - } - - /** - * See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link - * ReceiverAppStateUpdate#currentPlaybackPositionMs}. - */ - public Builder setMediaItemsInformation(Map mediaItemsInformation) { - this.mediaItemsInformation = Collections.unmodifiableMap(mediaItemsInformation); - return this; - } - - /** See {@link ReceiverAppStateUpdate#shuffleOrder}. */ - public Builder setShuffleOrder(List shuffleOrder) { - this.shuffleOrder = Collections.unmodifiableList(shuffleOrder); - return this; - } - - /** - * Returns a new {@link ReceiverAppStateUpdate} instance with the current values in this - * builder. - */ - public ReceiverAppStateUpdate build() { - return new ReceiverAppStateUpdate( - sequenceNumber, - playWhenReady, - playbackState, - items, - repeatMode, - shuffleModeEnabled, - isLoading, - playbackParameters, - trackSelectionParameters, - errorMessage, - discontinuityReason, - currentPlayingItemUuid, - currentPlayingPeriodId, - currentPlaybackPositionMs, - mediaItemsInformation, - shuffleOrder); - } - } - - /** Returns a {@link ReceiverAppStateUpdate} builder. */ - public static Builder builder(long sequenceNumber) { - return new Builder(sequenceNumber); - } - - /** - * Creates an instance from parsing a state update received from the Receiver App. - * - * @param jsonMessage The state update encoded as a JSON string. - * @return The parsed state update. - * @throws JSONException If an error is encountered when parsing the {@code jsonMessage}. - */ - public static ReceiverAppStateUpdate fromJsonMessage(String jsonMessage) throws JSONException { - JSONObject stateAsJson = new JSONObject(jsonMessage); - Builder builder = builder(stateAsJson.getLong(KEY_SEQUENCE_NUMBER)); - - if (stateAsJson.has(KEY_PLAY_WHEN_READY)) { - builder.setPlayWhenReady(stateAsJson.getBoolean(KEY_PLAY_WHEN_READY)); - } - - if (stateAsJson.has(KEY_PLAYBACK_STATE)) { - builder.setPlaybackState( - playbackStateStringToConstant(stateAsJson.getString(KEY_PLAYBACK_STATE))); - } - - if (stateAsJson.has(KEY_MEDIA_QUEUE)) { - builder.setItems( - toMediaItemArrayList(Assertions.checkNotNull(stateAsJson.optJSONArray(KEY_MEDIA_QUEUE)))); - } - - if (stateAsJson.has(KEY_REPEAT_MODE)) { - builder.setRepeatMode(stringToRepeatMode(stateAsJson.getString(KEY_REPEAT_MODE))); - } - - if (stateAsJson.has(KEY_SHUFFLE_MODE_ENABLED)) { - builder.setShuffleModeEnabled(stateAsJson.getBoolean(KEY_SHUFFLE_MODE_ENABLED)); - } - - if (stateAsJson.has(KEY_IS_LOADING)) { - builder.setIsLoading(stateAsJson.getBoolean(KEY_IS_LOADING)); - } - - if (stateAsJson.has(KEY_PLAYBACK_PARAMETERS)) { - builder.setPlaybackParameters( - toPlaybackParameters( - Assertions.checkNotNull(stateAsJson.optJSONObject(KEY_PLAYBACK_PARAMETERS)))); - } - - if (stateAsJson.has(KEY_TRACK_SELECTION_PARAMETERS)) { - JSONObject trackSelectionParametersJson = - stateAsJson.getJSONObject(KEY_TRACK_SELECTION_PARAMETERS); - TrackSelectionParameters parameters = - TrackSelectionParameters.DEFAULT - .buildUpon() - .setPreferredTextLanguage( - trackSelectionParametersJson.getString(KEY_PREFERRED_TEXT_LANGUAGE)) - .setPreferredAudioLanguage( - trackSelectionParametersJson.getString(KEY_PREFERRED_AUDIO_LANGUAGE)) - .setSelectUndeterminedTextLanguage( - trackSelectionParametersJson.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)) - .setDisabledTextTrackSelectionFlags( - jsonArrayToSelectionFlags( - trackSelectionParametersJson.getJSONArray( - KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS))) - .build(); - builder.setTrackSelectionParameters(parameters); - } - - if (stateAsJson.has(KEY_ERROR_MESSAGE)) { - builder.setErrorMessage(stateAsJson.getString(KEY_ERROR_MESSAGE)); - } - - if (stateAsJson.has(KEY_PLAYBACK_POSITION)) { - JSONObject playbackPosition = stateAsJson.getJSONObject(KEY_PLAYBACK_POSITION); - String discontinuityReason = playbackPosition.optString(KEY_DISCONTINUITY_REASON); - if (!discontinuityReason.isEmpty()) { - builder.setDiscontinuityReason(stringToDiscontinuityReason(discontinuityReason)); - } - UUID currentPlayingItemUuid = UUID.fromString(playbackPosition.getString(KEY_UUID)); - String currentPlayingPeriodId = playbackPosition.getString(KEY_PERIOD_ID); - Long currentPlaybackPositionMs = playbackPosition.getLong(KEY_POSITION_MS); - builder.setPlaybackPosition( - currentPlayingItemUuid, currentPlayingPeriodId, currentPlaybackPositionMs); - } - - if (stateAsJson.has(KEY_MEDIA_ITEMS_INFO)) { - HashMap mediaItemInformation = new HashMap<>(); - JSONObject mediaItemsInfo = stateAsJson.getJSONObject(KEY_MEDIA_ITEMS_INFO); - for (Iterator i = mediaItemsInfo.keys(); i.hasNext(); ) { - String key = i.next(); - mediaItemInformation.put( - UUID.fromString(key), jsonToMediaitemInfo(mediaItemsInfo.getJSONObject(key))); - } - builder.setMediaItemsInformation(mediaItemInformation); - } - - if (stateAsJson.has(KEY_SHUFFLE_ORDER)) { - ArrayList shuffleOrder = new ArrayList<>(); - JSONArray shuffleOrderJson = stateAsJson.getJSONArray(KEY_SHUFFLE_ORDER); - for (int i = 0; i < shuffleOrderJson.length(); i++) { - shuffleOrder.add(shuffleOrderJson.getInt(i)); - } - builder.setShuffleOrder(shuffleOrder); - } - - return builder.build(); - } - - /** The sequence number of the status update. */ - public final long sequenceNumber; - /** Optional {@link Player#getPlayWhenReady playWhenReady} value. */ - @Nullable public final Boolean playWhenReady; - /** Optional {@link Player#getPlaybackState() playbackState}. */ - @Nullable public final Integer playbackState; - /** Optional list of media items. */ - @Nullable public final List items; - /** Optional {@link Player#getRepeatMode() repeatMode}. */ - @Nullable public final Integer repeatMode; - /** Optional {@link Player#getShuffleModeEnabled() shuffleMode}. */ - @Nullable public final Boolean shuffleModeEnabled; - /** Optional {@link Player#isLoading() isLoading} value. */ - @Nullable public final Boolean isLoading; - /** Optional {@link Player#getPlaybackParameters() playbackParameters}. */ - @Nullable public final PlaybackParameters playbackParameters; - /** Optional {@link TrackSelectionParameters}. */ - @Nullable public final TrackSelectionParameters trackSelectionParameters; - /** Optional error message string. */ - @Nullable public final String errorMessage; - /** - * Optional reason for a {@link Player.EventListener#onPositionDiscontinuity(int) discontinuity } - * in the playback position. - */ - @Nullable public final Integer discontinuityReason; - /** Optional {@link UUID} of the {@link Player#getCurrentWindowIndex() currently played item}. */ - @Nullable public final UUID currentPlayingItemUuid; - /** Optional id of the current {@link Player#getCurrentPeriodIndex() period being played}. */ - @Nullable public final String currentPlayingPeriodId; - /** Optional {@link Player#getCurrentPosition() playbackPosition} in milliseconds. */ - @Nullable public final Long currentPlaybackPositionMs; - /** Holds information about the {@link MediaItem media items} in the media queue. */ - public final Map mediaItemsInformation; - /** Holds the indices of the media queue items in shuffle order. */ - @Nullable public final List shuffleOrder; - - /** Creates an instance with the given values. */ - private ReceiverAppStateUpdate( - long sequenceNumber, - @Nullable Boolean playWhenReady, - @Nullable Integer playbackState, - @Nullable List items, - @Nullable Integer repeatMode, - @Nullable Boolean shuffleModeEnabled, - @Nullable Boolean isLoading, - @Nullable PlaybackParameters playbackParameters, - @Nullable TrackSelectionParameters trackSelectionParameters, - @Nullable String errorMessage, - @Nullable Integer discontinuityReason, - @Nullable UUID currentPlayingItemUuid, - @Nullable String currentPlayingPeriodId, - @Nullable Long currentPlaybackPositionMs, - Map mediaItemsInformation, - @Nullable List shuffleOrder) { - this.sequenceNumber = sequenceNumber; - this.playWhenReady = playWhenReady; - this.playbackState = playbackState; - this.items = items; - this.repeatMode = repeatMode; - this.shuffleModeEnabled = shuffleModeEnabled; - this.isLoading = isLoading; - this.playbackParameters = playbackParameters; - this.trackSelectionParameters = trackSelectionParameters; - this.errorMessage = errorMessage; - this.discontinuityReason = discontinuityReason; - this.currentPlayingItemUuid = currentPlayingItemUuid; - this.currentPlayingPeriodId = currentPlayingPeriodId; - this.currentPlaybackPositionMs = currentPlaybackPositionMs; - this.mediaItemsInformation = mediaItemsInformation; - this.shuffleOrder = shuffleOrder; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - ReceiverAppStateUpdate that = (ReceiverAppStateUpdate) other; - - return sequenceNumber == that.sequenceNumber - && Util.areEqual(playWhenReady, that.playWhenReady) - && Util.areEqual(playbackState, that.playbackState) - && Util.areEqual(items, that.items) - && Util.areEqual(repeatMode, that.repeatMode) - && Util.areEqual(shuffleModeEnabled, that.shuffleModeEnabled) - && Util.areEqual(isLoading, that.isLoading) - && Util.areEqual(playbackParameters, that.playbackParameters) - && Util.areEqual(trackSelectionParameters, that.trackSelectionParameters) - && Util.areEqual(errorMessage, that.errorMessage) - && Util.areEqual(discontinuityReason, that.discontinuityReason) - && Util.areEqual(currentPlayingItemUuid, that.currentPlayingItemUuid) - && Util.areEqual(currentPlayingPeriodId, that.currentPlayingPeriodId) - && Util.areEqual(currentPlaybackPositionMs, that.currentPlaybackPositionMs) - && Util.areEqual(mediaItemsInformation, that.mediaItemsInformation) - && Util.areEqual(shuffleOrder, that.shuffleOrder); - } - - @Override - public int hashCode() { - int result = (int) (sequenceNumber ^ (sequenceNumber >>> 32)); - result = 31 * result + (playWhenReady != null ? playWhenReady.hashCode() : 0); - result = 31 * result + (playbackState != null ? playbackState.hashCode() : 0); - result = 31 * result + (items != null ? items.hashCode() : 0); - result = 31 * result + (repeatMode != null ? repeatMode.hashCode() : 0); - result = 31 * result + (shuffleModeEnabled != null ? shuffleModeEnabled.hashCode() : 0); - result = 31 * result + (isLoading != null ? isLoading.hashCode() : 0); - result = 31 * result + (playbackParameters != null ? playbackParameters.hashCode() : 0); - result = - 31 * result + (trackSelectionParameters != null ? trackSelectionParameters.hashCode() : 0); - result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0); - result = 31 * result + (discontinuityReason != null ? discontinuityReason.hashCode() : 0); - result = 31 * result + (currentPlayingItemUuid != null ? currentPlayingItemUuid.hashCode() : 0); - result = 31 * result + (currentPlayingPeriodId != null ? currentPlayingPeriodId.hashCode() : 0); - result = - 31 * result - + (currentPlaybackPositionMs != null ? currentPlaybackPositionMs.hashCode() : 0); - result = 31 * result + mediaItemsInformation.hashCode(); - result = 31 * result + (shuffleOrder != null ? shuffleOrder.hashCode() : 0); - return result; - } - - // Internal methods. - - @VisibleForTesting - /* package */ static List toMediaItemArrayList(JSONArray mediaItemsAsJson) - throws JSONException { - ArrayList mediaItems = new ArrayList<>(); - for (int i = 0; i < mediaItemsAsJson.length(); i++) { - mediaItems.add(toMediaItem(mediaItemsAsJson.getJSONObject(i))); - } - return mediaItems; - } - - private static MediaItem toMediaItem(JSONObject mediaItemAsJson) throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - builder.setUuid(UUID.fromString(mediaItemAsJson.getString(KEY_UUID))); - builder.setTitle(mediaItemAsJson.getString(KEY_TITLE)); - builder.setDescription(mediaItemAsJson.getString(KEY_DESCRIPTION)); - builder.setMedia(jsonToUriBundle(mediaItemAsJson.getJSONObject(KEY_MEDIA))); - // TODO(Internal b/118431961): Add attachment management. - - builder.setDrmSchemes(jsonArrayToDrmSchemes(mediaItemAsJson.getJSONArray(KEY_DRM_SCHEMES))); - if (mediaItemAsJson.has(KEY_START_POSITION_US)) { - builder.setStartPositionUs(mediaItemAsJson.getLong(KEY_START_POSITION_US)); - } - if (mediaItemAsJson.has(KEY_END_POSITION_US)) { - builder.setEndPositionUs(mediaItemAsJson.getLong(KEY_END_POSITION_US)); - } - builder.setMimeType(mediaItemAsJson.getString(KEY_MIME_TYPE)); - return builder.build(); - } - - private static PlaybackParameters toPlaybackParameters(JSONObject parameters) - throws JSONException { - float speed = (float) parameters.getDouble(KEY_SPEED); - float pitch = (float) parameters.getDouble(KEY_PITCH); - boolean skipSilence = parameters.getBoolean(KEY_SKIP_SILENCE); - return new PlaybackParameters(speed, pitch, skipSilence); - } - - private static int playbackStateStringToConstant(String string) { - switch (string) { - case STR_STATE_IDLE: - return STATE_IDLE; - case STR_STATE_BUFFERING: - return STATE_BUFFERING; - case STR_STATE_READY: - return STATE_READY; - case STR_STATE_ENDED: - return STATE_ENDED; - default: - throw new AssertionError("Unexpected state string: " + string); - } - } - - private static Integer stringToRepeatMode(String repeatModeStr) { - switch (repeatModeStr) { - case STR_REPEAT_MODE_OFF: - return REPEAT_MODE_OFF; - case STR_REPEAT_MODE_ONE: - return REPEAT_MODE_ONE; - case STR_REPEAT_MODE_ALL: - return REPEAT_MODE_ALL; - default: - throw new AssertionError("Illegal repeat mode: " + repeatModeStr); - } - } - - private static Integer stringToDiscontinuityReason(String discontinuityReasonStr) { - switch (discontinuityReasonStr) { - case STR_DISCONTINUITY_REASON_PERIOD_TRANSITION: - return DISCONTINUITY_REASON_PERIOD_TRANSITION; - case STR_DISCONTINUITY_REASON_SEEK: - return DISCONTINUITY_REASON_SEEK; - case STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT: - return DISCONTINUITY_REASON_SEEK_ADJUSTMENT; - case STR_DISCONTINUITY_REASON_AD_INSERTION: - return DISCONTINUITY_REASON_AD_INSERTION; - case STR_DISCONTINUITY_REASON_INTERNAL: - return DISCONTINUITY_REASON_INTERNAL; - default: - throw new AssertionError("Illegal discontinuity reason: " + discontinuityReasonStr); - } - } - - @C.SelectionFlags - private static int jsonArrayToSelectionFlags(JSONArray array) throws JSONException { - int result = 0; - for (int i = 0; i < array.length(); i++) { - switch (array.getString(i)) { - case ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT: - result |= C.SELECTION_FLAG_AUTOSELECT; - break; - case ExoCastConstants.STR_SELECTION_FLAG_FORCED: - result |= C.SELECTION_FLAG_FORCED; - break; - case ExoCastConstants.STR_SELECTION_FLAG_DEFAULT: - result |= C.SELECTION_FLAG_DEFAULT; - break; - default: - // Do nothing. - break; - } - } - return result; - } - - private static List jsonArrayToDrmSchemes(JSONArray drmSchemesAsJson) - throws JSONException { - ArrayList drmSchemes = new ArrayList<>(); - for (int i = 0; i < drmSchemesAsJson.length(); i++) { - JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); - MediaItem.UriBundle uriBundle = - drmSchemeAsJson.has(KEY_LICENSE_SERVER) - ? jsonToUriBundle(drmSchemeAsJson.getJSONObject(KEY_LICENSE_SERVER)) - : null; - drmSchemes.add( - new MediaItem.DrmScheme(UUID.fromString(drmSchemeAsJson.getString(KEY_UUID)), uriBundle)); - } - return Collections.unmodifiableList(drmSchemes); - } - - private static MediaItem.UriBundle jsonToUriBundle(JSONObject json) throws JSONException { - Uri uri = Uri.parse(json.getString(KEY_URI)); - JSONObject requestHeadersAsJson = json.getJSONObject(KEY_REQUEST_HEADERS); - HashMap requestHeaders = new HashMap<>(); - for (Iterator i = requestHeadersAsJson.keys(); i.hasNext(); ) { - String key = i.next(); - requestHeaders.put(key, requestHeadersAsJson.getString(key)); - } - return new MediaItem.UriBundle(uri, requestHeaders); - } - - private static MediaItemInfo jsonToMediaitemInfo(JSONObject json) throws JSONException { - long durationUs = json.getLong(KEY_WINDOW_DURATION_US); - long defaultPositionUs = json.optLong(KEY_DEFAULT_START_POSITION_US, /* fallback= */ 0L); - JSONArray periodsJson = json.getJSONArray(KEY_PERIODS); - ArrayList periods = new ArrayList<>(); - long positionInFirstPeriodUs = json.getLong(KEY_POSITION_IN_FIRST_PERIOD_US); - - long windowPositionUs = -positionInFirstPeriodUs; - for (int i = 0; i < periodsJson.length(); i++) { - JSONObject periodJson = periodsJson.getJSONObject(i); - long periodDurationUs = periodJson.optLong(KEY_DURATION_US, C.TIME_UNSET); - periods.add( - new MediaItemInfo.Period( - periodJson.getString(KEY_ID), periodDurationUs, windowPositionUs)); - windowPositionUs += periodDurationUs; - } - boolean isDynamic = json.getBoolean(KEY_IS_DYNAMIC); - boolean isSeekable = json.getBoolean(KEY_IS_SEEKABLE); - return new MediaItemInfo( - durationUs, defaultPositionUs, periods, positionInFirstPeriodUs, isSeekable, isDynamic); - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java deleted file mode 100644 index b900a78937..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java +++ /dev/null @@ -1,436 +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; - -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE; -import static com.google.common.truth.Truth.assertThat; - -import android.net.Uri; -import androidx.annotation.Nullable; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.cast.MediaItem.DrmScheme; -import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import com.google.android.exoplayer2.util.MimeTypes; -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 java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link ExoCastMessage}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastMessageTest { - - @Test - public void addItems_withUnsetIndex_doesNotAddIndexToJson() throws JSONException { - MediaItem sampleItem = new MediaItem.Builder().build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Collections.singletonList(sampleItem), - new ShuffleOrder.UnshuffledShuffleOrder(1)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); - assertThat(arguments.has(KEY_INDEX)).isFalse(); - assertThat(items.length()).isEqualTo(1); - } - - @Test - public void addItems_withMultipleItems_producesExpectedJsonList() throws JSONException { - MediaItem sampleItem1 = new MediaItem.Builder().build(); - MediaItem sampleItem2 = new MediaItem.Builder().build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - 1, Arrays.asList(sampleItem2, sampleItem1), new ShuffleOrder.UnshuffledShuffleOrder(2)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS); - assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(1); - assertThat(items.length()).isEqualTo(2); - } - - @Test - public void addItems_withoutItemOptionalFields_doesNotAddFieldsToJson() throws JSONException { - MediaItem itemWithoutOptionalFields = - new MediaItem.Builder() - .setTitle("title") - .setMimeType(MimeTypes.AUDIO_MP4) - .setDescription("desc") - .setDrmSchemes(Collections.singletonList(new DrmScheme(C.WIDEVINE_UUID, null))) - .setMedia("www.google.com") - .build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Collections.singletonList(itemWithoutOptionalFields), - new ShuffleOrder.UnshuffledShuffleOrder(1)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithoutOptionalFields); - } - - @Test - public void addItems_withAllItemFields_addsFieldsToJson() throws JSONException { - HashMap headersMedia = new HashMap<>(); - headersMedia.put("header1", "value1"); - headersMedia.put("header2", "value2"); - UriBundle media = new UriBundle(Uri.parse("www.google.com"), headersMedia); - - HashMap headersWidevine = new HashMap<>(); - headersWidevine.put("widevine", "value"); - UriBundle widevingUriBundle = new UriBundle(Uri.parse("www.widevine.com"), headersWidevine); - - HashMap headersPlayready = new HashMap<>(); - headersPlayready.put("playready", "value"); - UriBundle playreadyUriBundle = new UriBundle(Uri.parse("www.playready.com"), headersPlayready); - - DrmScheme[] drmSchemes = - new DrmScheme[] { - new DrmScheme(C.WIDEVINE_UUID, widevingUriBundle), - new DrmScheme(C.PLAYREADY_UUID, playreadyUriBundle) - }; - MediaItem itemWithAllFields = - new MediaItem.Builder() - .setTitle("title") - .setMimeType(MimeTypes.VIDEO_MP4) - .setDescription("desc") - .setStartPositionUs(3) - .setEndPositionUs(10) - .setDrmSchemes(Arrays.asList(drmSchemes)) - .setMedia(media) - .build(); - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Collections.singletonList(itemWithAllFields), - new ShuffleOrder.UnshuffledShuffleOrder(1)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - JSONArray items = arguments.getJSONArray(KEY_ITEMS); - - assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithAllFields); - } - - @Test - public void addItems_withShuffleOrder_producesExpectedJson() throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - MediaItem sampleItem1 = builder.build(); - MediaItem sampleItem2 = builder.build(); - MediaItem sampleItem3 = builder.build(); - MediaItem sampleItem4 = builder.build(); - - ExoCastMessage message = - new ExoCastMessage.AddItems( - C.INDEX_UNSET, - Arrays.asList(sampleItem1, sampleItem2, sampleItem3, sampleItem4), - new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); - JSONObject arguments = - new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)).getJSONObject(KEY_ARGS); - JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); - assertThat(shuffledIndices.getInt(0)).isEqualTo(2); - assertThat(shuffledIndices.getInt(1)).isEqualTo(1); - assertThat(shuffledIndices.getInt(2)).isEqualTo(3); - assertThat(shuffledIndices.getInt(3)).isEqualTo(0); - } - - @Test - public void moveItem_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.MoveItem( - new UUID(0, 1), - /* index= */ 3, - new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_MOVE_ITEM); - assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); - assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(3); - JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER); - assertThat(shuffledIndices.getInt(0)).isEqualTo(2); - assertThat(shuffledIndices.getInt(1)).isEqualTo(1); - assertThat(shuffledIndices.getInt(2)).isEqualTo(3); - assertThat(shuffledIndices.getInt(3)).isEqualTo(0); - } - - @Test - public void removeItems_withSingleItem_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.RemoveItems(Collections.singletonList(new UUID(0, 1))); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); - assertThat(uuids.length()).isEqualTo(1); - assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); - } - - @Test - public void removeItems_withMultipleItems_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.RemoveItems( - Arrays.asList(new UUID(0, 1), new UUID(0, 2), new UUID(0, 3))); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS); - assertThat(uuids.length()).isEqualTo(3); - assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString()); - assertThat(uuids.getString(1)).isEqualTo(new UUID(0, 2).toString()); - assertThat(uuids.getString(2)).isEqualTo(new UUID(0, 3).toString()); - } - - @Test - public void setPlayWhenReady_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetPlayWhenReady(true); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAY_WHEN_READY); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_PLAY_WHEN_READY)).isTrue(); - } - - @Test - public void setRepeatMode_withRepeatModeOff_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_OFF); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) - .isEqualTo(STR_REPEAT_MODE_OFF); - } - - @Test - public void setRepeatMode_withRepeatModeOne_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ONE); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) - .isEqualTo(STR_REPEAT_MODE_ONE); - } - - @Test - public void setRepeatMode_withRepeatModeAll_producesExpectedJson() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ALL); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE)) - .isEqualTo(STR_REPEAT_MODE_ALL); - } - - @Test - public void setShuffleModeEnabled_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SetShuffleModeEnabled(/* shuffleModeEnabled= */ false); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_SHUFFLE_MODE_ENABLED); - assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_SHUFFLE_MODE_ENABLED)) - .isFalse(); - } - - @Test - public void seekTo_withPositionInItem_addsPositionField() throws JSONException { - ExoCastMessage message = new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ 10); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); - assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); - assertThat(arguments.getLong(KEY_POSITION_MS)).isEqualTo(10); - } - - @Test - public void seekTo_withUnsetPosition_doesNotAddPositionField() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ C.TIME_UNSET); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO); - assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString()); - assertThat(arguments.has(KEY_POSITION_MS)).isFalse(); - } - - @Test - public void setPlaybackParameters_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SetPlaybackParameters( - new PlaybackParameters(/* speed= */ 0.5f, /* pitch= */ 2, /* skipSilence= */ false)); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAYBACK_PARAMETERS); - assertThat(arguments.getDouble(KEY_SPEED)).isEqualTo(0.5); - assertThat(arguments.getDouble(KEY_PITCH)).isEqualTo(2.0); - assertThat(arguments.getBoolean(KEY_SKIP_SILENCE)).isFalse(); - } - - @Test - public void setSelectionParameters_producesExpectedJson() throws JSONException { - ExoCastMessage message = - new ExoCastMessage.SetTrackSelectionParameters( - TrackSelectionParameters.DEFAULT - .buildUpon() - .setDisabledTextTrackSelectionFlags( - C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT) - .setSelectUndeterminedTextLanguage(true) - .setPreferredAudioLanguage("esp") - .setPreferredTextLanguage("deu") - .build()); - JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)); - JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS); - - assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0); - assertThat(messageAsJson.getString(KEY_METHOD)) - .isEqualTo(METHOD_SET_TRACK_SELECTION_PARAMETERS); - assertThat(arguments.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)).isTrue(); - assertThat(arguments.getString(KEY_PREFERRED_AUDIO_LANGUAGE)).isEqualTo("esp"); - assertThat(arguments.getString(KEY_PREFERRED_TEXT_LANGUAGE)).isEqualTo("deu"); - ArrayList selectionFlagStrings = new ArrayList<>(); - JSONArray selectionFlagsJson = arguments.getJSONArray(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS); - for (int i = 0; i < selectionFlagsJson.length(); i++) { - selectionFlagStrings.add(selectionFlagsJson.getString(i)); - } - assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT); - assertThat(selectionFlagStrings).doesNotContain(ExoCastConstants.STR_SELECTION_FLAG_FORCED); - assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT); - } - - private static void assertJsonEqualsMediaItem(JSONObject itemAsJson, MediaItem mediaItem) - throws JSONException { - assertThat(itemAsJson.getString(KEY_UUID)).isEqualTo(mediaItem.uuid.toString()); - assertThat(itemAsJson.getString(KEY_TITLE)).isEqualTo(mediaItem.title); - assertThat(itemAsJson.getString(KEY_MIME_TYPE)).isEqualTo(mediaItem.mimeType); - assertThat(itemAsJson.getString(KEY_DESCRIPTION)).isEqualTo(mediaItem.description); - assertJsonMatchesTimestamp(itemAsJson, KEY_START_POSITION_US, mediaItem.startPositionUs); - assertJsonMatchesTimestamp(itemAsJson, KEY_END_POSITION_US, mediaItem.endPositionUs); - assertJsonMatchesUriBundle(itemAsJson, KEY_MEDIA, mediaItem.media); - - List drmSchemes = mediaItem.drmSchemes; - int drmSchemesLength = drmSchemes.size(); - JSONArray drmSchemesAsJson = itemAsJson.getJSONArray(KEY_DRM_SCHEMES); - - assertThat(drmSchemesAsJson.length()).isEqualTo(drmSchemesLength); - for (int i = 0; i < drmSchemesLength; i++) { - DrmScheme drmScheme = drmSchemes.get(i); - JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i); - - assertThat(drmSchemeAsJson.getString(KEY_UUID)).isEqualTo(drmScheme.uuid.toString()); - assertJsonMatchesUriBundle(drmSchemeAsJson, KEY_LICENSE_SERVER, drmScheme.licenseServer); - } - } - - private static void assertJsonMatchesUriBundle( - JSONObject jsonObject, String key, @Nullable UriBundle uriBundle) throws JSONException { - if (uriBundle == null) { - assertThat(jsonObject.has(key)).isFalse(); - return; - } - JSONObject uriBundleAsJson = jsonObject.getJSONObject(key); - assertThat(uriBundleAsJson.getString(KEY_URI)).isEqualTo(uriBundle.uri.toString()); - Map requestHeaders = uriBundle.requestHeaders; - JSONObject requestHeadersAsJson = uriBundleAsJson.getJSONObject(KEY_REQUEST_HEADERS); - - assertThat(requestHeadersAsJson.length()).isEqualTo(requestHeaders.size()); - for (String headerKey : requestHeaders.keySet()) { - assertThat(requestHeadersAsJson.getString(headerKey)) - .isEqualTo(requestHeaders.get(headerKey)); - } - } - - private static void assertJsonMatchesTimestamp(JSONObject object, String key, long timestamp) - throws JSONException { - if (timestamp == C.TIME_UNSET) { - assertThat(object.has(key)).isFalse(); - } else { - assertThat(object.getLong(key)).isEqualTo(timestamp); - } - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java deleted file mode 100644 index 58f78b090a..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java +++ /dev/null @@ -1,1018 +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; - -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS; -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.testutil.FakeClock; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; - -/** Unit test for {@link ExoCastPlayer}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastPlayerTest { - - private static final long MOCK_SEQUENCE_NUMBER = 1; - private ExoCastPlayer player; - private MediaItem.Builder itemBuilder; - private CastSessionManager.StateListener receiverAppStateListener; - private FakeClock clock; - @Mock private CastSessionManager sessionManager; - @Mock private SessionAvailabilityListener sessionAvailabilityListener; - @Mock private Player.EventListener playerEventListener; - - @Before - public void setUp() { - initMocks(this); - clock = new FakeClock(/* initialTimeMs= */ 0); - player = - new ExoCastPlayer( - listener -> { - receiverAppStateListener = listener; - return sessionManager; - }, - clock); - player.addListener(playerEventListener); - itemBuilder = new MediaItem.Builder(); - } - - @Test - public void exoCastPlayer_startsAndStopsSessionManager() { - // The session manager should have been started when setting up, with the creation of - // ExoCastPlayer. - verify(sessionManager).start(); - verifyNoMoreInteractions(sessionManager); - player.release(); - verify(sessionManager).stopTrackingSession(); - verifyNoMoreInteractions(sessionManager); - } - - @Test - public void exoCastPlayer_propagatesSessionStatus() { - player.setSessionAvailabilityListener(sessionAvailabilityListener); - verify(sessionAvailabilityListener, never()).onCastSessionAvailable(); - receiverAppStateListener.onCastSessionAvailable(); - verify(sessionAvailabilityListener).onCastSessionAvailable(); - verifyNoMoreInteractions(sessionAvailabilityListener); - receiverAppStateListener.onCastSessionUnavailable(); - verify(sessionAvailabilityListener).onCastSessionUnavailable(); - verifyNoMoreInteractions(sessionAvailabilityListener); - } - - @Test - public void addItemsToQueue_producesExpectedMessages() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); - - player.addItemsToQueue(item1, item2); - assertMediaItemQueue(item1, item2); - - player.addItemsToQueue(1, item3, item4); - assertMediaItemQueue(item1, item3, item4, item2); - - player.addItemsToQueue(item5); - assertMediaItemQueue(item1, item3, item4, item2, item5); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(3)).send(messageCaptor.capture()); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(0), - /* index= */ C.INDEX_UNSET, - Arrays.asList(item1, item2)); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(1), - /* index= */ 1, - Arrays.asList(item3, item4)); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(2), - /* index= */ C.INDEX_UNSET, - Collections.singletonList(item5)); - } - - @Test - public void addItemsToQueue_masksRemoteUpdates() { - player.prepare(); - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - player.addItemsToQueue(item1, item2); - assertMediaItemQueue(item1, item2); - - // Should be ignored due to a lower sequence number. - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setItems(Arrays.asList(item3, item4)) - .build()); - - // Should override the current state. - assertMediaItemQueue(item1, item2); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setItems(Arrays.asList(item3, item4)) - .build()); - - assertMediaItemQueue(item3, item4); - } - - @Test - public void addItemsToQueue_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - player.addItemsToQueue(/* optionalIndex= */ 0, itemBuilder.build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(3); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - - player.addItemsToQueue(itemBuilder.build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(3); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - } - - @Test - public void addItemsToQueue_doesNotAddDuplicateUuids() { - player.prepare(); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - assertThat(player.getQueueSize()).isEqualTo(1); - player.addItemsToQueue( - itemBuilder.setUuid(toUuid(1)).build(), itemBuilder.setUuid(toUuid(2)).build()); - assertThat(player.getQueueSize()).isEqualTo(2); - try { - player.addItemsToQueue( - itemBuilder.setUuid(toUuid(3)).build(), itemBuilder.setUuid(toUuid(3)).build()); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } - } - - @Test - public void moveItemInQueue_behavesAsExpected() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - player.addItemsToQueue(item1, item2, item3); - assertMediaItemQueue(item1, item2, item3); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); - assertMediaItemQueue(item2, item3, item1); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 1); - assertMediaItemQueue(item2, item3, item1); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); - assertMediaItemQueue(item3, item2, item1); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(4)).send(messageCaptor.capture()); - // First sent message is an "add" message. - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(1), item1, /* index= */ 2); - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(2), item3, /* index= */ 1); - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(3), item3, /* index= */ 0); - } - - @Test - public void moveItemInQueue_moveBeforeToAfter_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 1); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void moveItemInQueue_moveAfterToBefore_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - } - - @Test - public void moveItemInQueue_moveCurrent_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - } - - @Test - public void removeItemsFromQueue_masksMediaQueue() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); - player.addItemsToQueue(item1, item2, item3, item4, item5); - assertMediaItemQueue(item1, item2, item3, item4, item5); - - player.removeItemFromQueue(2); - assertMediaItemQueue(item1, item2, item4, item5); - - player.removeRangeFromQueue(1, 3); - assertMediaItemQueue(item1, item5); - - player.clearQueue(); - assertMediaItemQueue(); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(4)).send(messageCaptor.capture()); - // First sent message is an "add" message. - assertMessageRemovesItems( - messageCaptor.getAllValues().get(1), Collections.singletonList(item3)); - assertMessageRemovesItems(messageCaptor.getAllValues().get(2), Arrays.asList(item2, item4)); - assertMessageRemovesItems(messageCaptor.getAllValues().get(3), Arrays.asList(item1, item5)); - } - - @Test - public void removeRangeFromQueue_beforeCurrentItem_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void removeRangeFromQueue_currentItem_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void removeRangeFromQueue_currentItemWhichIsLast_transitionsToEnded() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.removeRangeFromQueue(/* indexFrom= */ 1, /* indexExclusiveTo= */ 3); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - } - - @Test - public void clearQueue_resetsPlaybackPosition() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.clearQueue(); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - } - - @Test - public void prepare_emptyQueue_transitionsToEnded() { - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_ENDED); - } - - @Test - public void prepare_withQueue_transitionsToBuffering() { - player.addItemsToQueue(itemBuilder.build()); - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_BUFFERING); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - } - - @Test - public void stop_withoutReset_leavesCurrentTimeline() { - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - player.stop(/* reset= */ false); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - // Update for prepare. - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - - // Update for stop. - verifyNoMoreInteractions(playerEventListener); - assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); - } - - @Test - public void stop_withReset_clearsQueue() { - player.prepare(); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - verify(playerEventListener) - .onTimelineChanged( - any(Timeline.class), isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); - player.stop(/* reset= */ true); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); - - // Update for add. - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - - // Update for stop. - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_RESET)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(0); - - assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); - } - - @Test - public void getCurrentTimeline_masksRemoteUpdates() { - player.prepare(); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); - player.addItemsToQueue(item1, item2); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - messageCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - Timeline reportedTimeline = messageCaptor.getValue(); - assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); - assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - } - - @Test - public void getCurrentTimeline_exposesReceiverState() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2)) - .setShuffleOrder(Arrays.asList(1, 0)) - .build()); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - messageCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - Timeline reportedTimeline = messageCaptor.getValue(); - assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); - assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - } - - @Test - public void timelineUpdateFromReceiver_matchesLocalState_doesNotCallEventLsitener() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period3 = - new MediaItemInfo.Period( - "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); - HashMap mediaItemInfoMap1 = new HashMap<>(); - mediaItemInfoMap1.put( - toUuid(1), - new MediaItemInfo( - /* windowDurationUs= */ 3000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false)); - mediaItemInfoMap1.put( - toUuid(3), - new MediaItemInfo( - /* windowDurationUs= */ 2000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2, item3, item4)) - .setShuffleOrder(Arrays.asList(1, 0, 2, 3)) - .setMediaItemsInformation(mediaItemInfoMap1) - .build()); - verify(playerEventListener) - .onTimelineChanged( - any(), /* manifest= */ isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - verify(playerEventListener) - .onPlayerStateChanged( - /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); - - HashMap mediaItemInfoMap2 = new HashMap<>(mediaItemInfoMap1); - mediaItemInfoMap2.put( - toUuid(5), - new MediaItemInfo( - /* windowDurationUs= */ 5, - /* defaultStartPositionUs= */ 0, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1).setMediaItemsInformation(mediaItemInfoMap2).build()); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void getPeriodIndex_producesExpectedOutput() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period3 = - new MediaItemInfo.Period( - "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); - HashMap mediaItemInfoMap = new HashMap<>(); - mediaItemInfoMap.put( - toUuid(1), - new MediaItemInfo( - /* windowDurationUs= */ 3000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false)); - mediaItemInfoMap.put( - toUuid(3), - new MediaItemInfo( - /* windowDurationUs= */ 2000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1L) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2, item3, item4)) - .setShuffleOrder(Arrays.asList(1, 0, 3, 2)) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition( - /* currentPlayingItemUuid= */ item3.uuid, - /* currentPlayingPeriodId= */ "id2", - /* currentPlaybackPositionMs= */ 500L) - .build()); - - assertThat(player.getCurrentPeriodIndex()).isEqualTo(5); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0L); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1500L); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - } - - @Test - public void exoCastPlayer_propagatesPlayerStateFromReceiver() { - ReceiverAppStateUpdate.Builder builder = - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1); - - // The first idle state update should be discarded, since it matches the current state. - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_IDLE).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_BUFFERING).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_READY).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_ENDED).build()); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Integer.class); - verify(playerEventListener, times(3)) - .onPlayerStateChanged(/* playWhenReady= */ eq(false), messageCaptor.capture()); - List states = messageCaptor.getAllValues(); - assertThat(states).hasSize(3); - assertThat(states) - .isEqualTo(Arrays.asList(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED)); - } - - @Test - public void setPlayWhenReady_changedLocally_notifiesListeners() { - player.setPlayWhenReady(false); - verify(playerEventListener, never()).onPlayerStateChanged(false, Player.STATE_IDLE); - player.setPlayWhenReady(true); - verify(playerEventListener).onPlayerStateChanged(true, Player.STATE_IDLE); - player.setPlayWhenReady(false); - verify(playerEventListener).onPlayerStateChanged(false, Player.STATE_IDLE); - } - - @Test - public void setPlayWhenReady_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(false).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void getPlayWhenReady_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setPlayWhenReady(true); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2).setPlayWhenReady(false).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(true).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(false).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); - } - - @Test - public void setRepeatMode_changedLocally_notifiesListeners() { - player.setRepeatMode(Player.REPEAT_MODE_OFF); - verifyNoMoreInteractions(playerEventListener); - player.setRepeatMode(Player.REPEAT_MODE_ONE); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - player.setRepeatMode(Player.REPEAT_MODE_ONE); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void setRepeatMode_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); - } - - @Test - public void getRepeatMode_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setRepeatMode(Player.REPEAT_MODE_ALL); - assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - } - - @Test - public void getPlaybackPosition_withStateChanges_producesExpectedOutput() { - UUID uuid = toUuid(1); - HashMap mediaItemInfoMap = new HashMap<>(); - - MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); - MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); - MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); - mediaItemInfoMap.put( - uuid, - new MediaItemInfo( - /* windowDurationUs= */ 1000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(1L); - player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) - .build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(1000L); - clock.advanceTime(/* timeDiffMs= */ 1L); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_READY) - .build()); - // Play when ready is still false, so position should not change. - assertThat(player.getCurrentPosition()).isEqualTo(1000L); - player.setPlayWhenReady(true); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1001L); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1002L); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1010L) - .build()); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1010L); - clock.advanceTime(1); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_READY) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1011L) - .build()); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(1021L); - } - - @Test - public void getPlaybackPosition_withNonDefaultPlaybackSpeed_producesExpectedOutput() { - MediaItem item = itemBuilder.setUuid(toUuid(1)).build(); - MediaItemInfo info = - new MediaItemInfo( - /* windowDurationUs= */ 10000000, - /* defaultStartPositionUs= */ 3000000, - /* periods= */ Collections.singletonList( - new MediaItemInfo.Period( - /* id= */ "id", /* durationUs= */ 10000000, /* positionInWindowUs= */ 0)), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setMediaItemsInformation(Collections.singletonMap(toUuid(1), info)) - .setShuffleOrder(Collections.singletonList(0)) - .setItems(Collections.singletonList(item)) - .setPlaybackPosition( - toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 20L) - .setPlaybackState(Player.STATE_READY) - .setPlayWhenReady(true) - .build()); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(20); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(30); - clock.advanceTime(10); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1) - .setPlaybackPosition( - toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 40L) - .setPlaybackParameters(new PlaybackParameters(2)) - .build()); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(60); - } - - @Test - public void positionChanges_notifiesDiscontinuities() { - UUID uuid = toUuid(1); - HashMap mediaItemInfoMap = new HashMap<>(); - - MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); - MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); - MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); - mediaItemInfoMap.put( - uuid, - new MediaItemInfo( - /* windowDurationUs= */ 1000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build()); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 999); - verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } - - @Test - public void setShuffleModeEnabled_changedLocally_notifiesListeners() { - player.setShuffleModeEnabled(true); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - player.setShuffleModeEnabled(true); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void setShuffleModeEnabled_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) - .setShuffleModeEnabled(true) - .build()); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - assertThat(player.getShuffleModeEnabled()).isTrue(); - } - - @Test - public void getShuffleMode_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setShuffleModeEnabled(true); - assertThat(player.getShuffleModeEnabled()).isTrue(); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setShuffleModeEnabled(false) - .build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setShuffleModeEnabled(false) - .build()); - verify(playerEventListener).onShuffleModeEnabledChanged(false); - assertThat(player.getShuffleModeEnabled()).isFalse(); - } - - @Test - public void seekTo_inIdle_doesNotChangePlaybackState() { - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build()); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - player.stop(false); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - } - - @Test - public void seekTo_withTwoItems_producesExpectedMessage() { - player.prepare(); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - player.addItemsToQueue(item1, item2); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(3)).send(messageCaptor.capture()); - // Messages should be prepare, add and seek. - ExoCastMessage.SeekTo seekToMessage = - (ExoCastMessage.SeekTo) messageCaptor.getAllValues().get(2); - assertThat(seekToMessage.positionMs).isEqualTo(1000); - assertThat(seekToMessage.uuid).isEqualTo(toUuid(2)); - } - - @Test - public void seekTo_masksRemoteUpdates() { - player.prepare(); - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - player.addItemsToQueue(item1, item2); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000L); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - verify(playerEventListener) - .onPlayerStateChanged( - /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPosition()).isEqualTo(1000); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setPlaybackPosition(toUuid(1), "id", 500L) - .build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPosition()).isEqualTo(1000); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setPlaybackPosition(toUuid(1), "id", 500L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build()); - verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(500); - } - - @Test - public void setPlaybackParameters_producesExpectedMessage() { - PlaybackParameters playbackParameters = - new PlaybackParameters(/* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ true); - player.setPlaybackParameters(playbackParameters); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager).send(messageCaptor.capture()); - ExoCastMessage.SetPlaybackParameters message = - (ExoCastMessage.SetPlaybackParameters) messageCaptor.getValue(); - assertThat(message.playbackParameters).isEqualTo(playbackParameters); - } - - @Test - public void getTrackSelectionParameters_doesNotOverrideUnexpectedFields() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - DefaultTrackSelector.Parameters parameters = - DefaultTrackSelector.Parameters.DEFAULT - .buildUpon() - .setPreferredAudioLanguage("spa") - .setMaxVideoSize(/* maxVideoWidth= */ 3, /* maxVideoHeight= */ 3) - .build(); - player.setTrackSelectionParameters(parameters); - TrackSelectionParameters returned = - TrackSelectionParameters.DEFAULT.buildUpon().setPreferredAudioLanguage("deu").build(); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setTrackSelectionParameters(returned) - .build()); - DefaultTrackSelector.Parameters result = - (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); - assertThat(result.preferredAudioLanguage).isEqualTo("deu"); - assertThat(result.maxVideoHeight).isEqualTo(3); - assertThat(result.maxVideoWidth).isEqualTo(3); - } - - @Test - public void testExoCast_getRendererType() { - assertThat(player.getRendererCount()).isEqualTo(4); - assertThat(player.getRendererType(/* index= */ 0)).isEqualTo(C.TRACK_TYPE_VIDEO); - assertThat(player.getRendererType(/* index= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO); - assertThat(player.getRendererType(/* index= */ 2)).isEqualTo(C.TRACK_TYPE_TEXT); - assertThat(player.getRendererType(/* index= */ 3)).isEqualTo(C.TRACK_TYPE_METADATA); - } - - private static UUID toUuid(long lowerBits) { - return new UUID(0, lowerBits); - } - - private void assertMediaItemQueue(MediaItem... mediaItemQueue) { - assertThat(player.getQueueSize()).isEqualTo(mediaItemQueue.length); - for (int i = 0; i < mediaItemQueue.length; i++) { - assertThat(player.getQueueItem(i).uuid).isEqualTo(mediaItemQueue[i].uuid); - } - } - - private static void assertMessageAddsItems( - ExoCastMessage message, int index, List mediaItems) throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_ADD_ITEMS); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - if (index != C.INDEX_UNSET) { - assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); - } else { - assertThat(args.has(KEY_INDEX)).isFalse(); - } - JSONArray itemsAsJson = args.getJSONArray(KEY_ITEMS); - assertThat(ReceiverAppStateUpdate.toMediaItemArrayList(itemsAsJson)).isEqualTo(mediaItems); - } - - private static void assertMessageMovesItem(ExoCastMessage message, MediaItem item, int index) - throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_MOVE_ITEM); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - assertThat(args.getString(KEY_UUID)).isEqualTo(item.uuid.toString()); - assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); - } - - private static void assertMessageRemovesItems(ExoCastMessage message, List items) - throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_REMOVE_ITEMS); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - JSONArray uuidsAsJson = args.getJSONArray(KEY_UUIDS); - for (int i = 0; i < uuidsAsJson.length(); i++) { - assertThat(uuidsAsJson.getString(i)).isEqualTo(items.get(i).uuid.toString()); - } - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java deleted file mode 100644 index f6084339e4..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java +++ /dev/null @@ -1,466 +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; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.UUID; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link ExoCastTimeline}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastTimelineTest { - - private MediaItem mediaItem1; - private MediaItem mediaItem2; - private MediaItem mediaItem3; - private MediaItem mediaItem4; - private MediaItem mediaItem5; - - @Before - public void setUp() { - MediaItem.Builder builder = new MediaItem.Builder(); - mediaItem1 = builder.setUuid(asUUID(1)).build(); - mediaItem2 = builder.setUuid(asUUID(2)).build(); - mediaItem3 = builder.setUuid(asUUID(3)).build(); - mediaItem4 = builder.setUuid(asUUID(4)).build(); - mediaItem5 = builder.setUuid(asUUID(5)).build(); - } - - @Test - public void getWindowCount_withNoItems_producesExpectedCount() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Collections.emptyList(), Collections.emptyMap(), new DefaultShuffleOrder(0)); - - assertThat(timeline.getWindowCount()).isEqualTo(0); - } - - @Test - public void getWindowCount_withFiveItems_producesExpectedCount() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - - assertThat(timeline.getWindowCount()).isEqualTo(5); - } - - @Test - public void getWindow_withNoMediaItemInfo_returnsEmptyWindow() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - Timeline.Window window = timeline.getWindow(2, new Timeline.Window(), /* setTag= */ true); - - assertThat(window.tag).isNull(); - assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.isSeekable).isFalse(); - assertThat(window.isDynamic).isTrue(); - assertThat(window.defaultPositionUs).isEqualTo(0L); - assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); - assertThat(window.firstPeriodIndex).isEqualTo(2); - assertThat(window.lastPeriodIndex).isEqualTo(2); - assertThat(window.positionInFirstPeriodUs).isEqualTo(0L); - } - - @Test - public void getWindow_withMediaItemInfo_returnsPopulatedWindow() { - MediaItem populatedMediaItem = new MediaItem.Builder().setAttachment("attachment").build(); - HashMap mediaItemInfos = new HashMap<>(); - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - mediaItemInfos.put( - populatedMediaItem.uuid, - new MediaItemInfo( - /* windowDurationUs= */ 4000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, populatedMediaItem), - mediaItemInfos, - new DefaultShuffleOrder(5)); - Timeline.Window window = timeline.getWindow(4, new Timeline.Window(), /* setTag= */ true); - - assertThat(window.tag).isSameInstanceAs(populatedMediaItem.attachment); - assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); - assertThat(window.isSeekable).isTrue(); - assertThat(window.isDynamic).isFalse(); - assertThat(window.defaultPositionUs).isEqualTo(20L); - assertThat(window.durationUs).isEqualTo(4000000L); - assertThat(window.firstPeriodIndex).isEqualTo(4); - assertThat(window.lastPeriodIndex).isEqualTo(5); - assertThat(window.positionInFirstPeriodUs).isEqualTo(500L); - } - - @Test - public void getPeriodCount_producesExpectedOutput() { - HashMap mediaItemInfos = new HashMap<>(); - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); - mediaItemInfos.put( - asUUID(2), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos, - new DefaultShuffleOrder(5)); - - assertThat(timeline.getPeriodCount()).isEqualTo(6); - } - - @Test - public void getPeriod_forPopulatedPeriod_producesExpectedOutput() { - HashMap mediaItemInfos = new HashMap<>(); - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L); - mediaItemInfos.put( - asUUID(5), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos, - new DefaultShuffleOrder(5)); - Timeline.Period period = - timeline.getPeriod(/* periodIndex= */ 5, new Timeline.Period(), /* setIds= */ true); - Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 5); - - assertThat(period.durationUs).isEqualTo(5000000L); - assertThat(period.windowIndex).isEqualTo(4); - assertThat(period.id).isEqualTo("id2"); - assertThat(period.uid).isEqualTo(periodUid); - } - - @Test - public void getPeriod_forEmptyPeriod_producesExpectedOutput() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - Timeline.Period period = timeline.getPeriod(2, new Timeline.Period(), /* setIds= */ true); - Object uid = timeline.getUidOfPeriod(/* periodIndex= */ 2); - - assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); - assertThat(period.windowIndex).isEqualTo(2); - assertThat(period.id).isEqualTo(MediaItemInfo.EMPTY.periods.get(0).id); - assertThat(period.uid).isEqualTo(uid); - } - - @Test - public void getIndexOfPeriod_worksAcrossDifferentTimelines() { - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - - HashMap mediaItemInfos1 = new HashMap<>(); - mediaItemInfos1.put( - asUUID(1), - new MediaItemInfo( - /* windowDurationUs= */ 5000000L, - /* defaultStartPositionUs= */ 20L, - Collections.singletonList(period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline1 = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2), mediaItemInfos1, new DefaultShuffleOrder(2)); - - HashMap mediaItemInfos2 = new HashMap<>(); - mediaItemInfos2.put( - asUUID(1), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline2 = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem2, mediaItem1, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos2, - new DefaultShuffleOrder(5)); - Object uidOfFirstPeriod = timeline1.getUidOfPeriod(0); - - assertThat(timeline1.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(0); - assertThat(timeline2.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(2); - } - - @Test - public void getIndexOfPeriod_forLastPeriod_producesExpectedOutput() { - MediaItemInfo.Period period1 = - new MediaItemInfo.Period( - "id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L); - - HashMap mediaItemInfos1 = new HashMap<>(); - mediaItemInfos1.put( - asUUID(5), - new MediaItemInfo( - /* windowDurationUs= */ 4000000L, - /* defaultStartPositionUs= */ 20L, - Collections.singletonList(period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline singlePeriodTimeline = - ExoCastTimeline.createTimelineFor( - Collections.singletonList(mediaItem5), mediaItemInfos1, new DefaultShuffleOrder(1)); - Object periodUid = singlePeriodTimeline.getUidOfPeriod(0); - - HashMap mediaItemInfos2 = new HashMap<>(); - mediaItemInfos2.put( - asUUID(5), - new MediaItemInfo( - /* windowDurationUs= */ 7000000L, - /* defaultStartPositionUs= */ 20L, - Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 0L, - /* isSeekable= */ true, - /* isDynamic= */ false)); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - mediaItemInfos2, - new DefaultShuffleOrder(5)); - - assertThat(timeline.getIndexOfPeriod(periodUid)).isEqualTo(5); - } - - @Test - public void getUidOfPeriod_withInvalidUid_returnsUnsetIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(/* length= */ 5)); - - assertThat(timeline.getIndexOfPeriod(new Object())).isEqualTo(C.INDEX_UNSET); - } - - @Test - public void getFirstWindowIndex_returnsIndexAccordingToShuffleMode() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(1); - } - - @Test - public void getLastWindowIndex_returnsIndexAccordingToShuffleMode() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(4); - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(3); - } - - @Test - public void getNextWindowIndex_repeatModeOne_returnsSameIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(5)); - - for (int i = 0; i < 5; i++) { - assertThat( - timeline.getNextWindowIndex( - i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) - .isEqualTo(i); - assertThat( - timeline.getNextWindowIndex( - i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) - .isEqualTo(i); - } - } - - @Test - public void getNextWindowIndex_onLastIndex_returnsExpectedIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - // Shuffle mode disabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) - .isEqualTo(0); - // Shuffle mode enabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 3, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 3, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) - .isEqualTo(1); - } - - @Test - public void getNextWindowIndex_inMiddleOfQueue_returnsNextIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - // Shuffle mode disabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) - .isEqualTo(3); - // Shuffle mode enabled: - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(0); - } - - @Test - public void getPreviousWindowIndex_repeatModeOne_returnsSameIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - for (int i = 0; i < 5; i++) { - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true)) - .isEqualTo(i); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false)) - .isEqualTo(i); - } - } - - @Test - public void getPreviousWindowIndex_onFirstIndex_returnsExpectedIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - // Shuffle mode disabled: - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) - .isEqualTo(4); - // Shuffle mode enabled: - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 1, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(C.INDEX_UNSET); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 1, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true)) - .isEqualTo(3); - } - - @Test - public void getPreviousWindowIndex_inMiddleOfQueue_returnsPreviousIndex() { - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5), - Collections.emptyMap(), - new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0)); - - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false)) - .isEqualTo(3); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(0); - } - - private static UUID asUUID(long number) { - return new UUID(/* mostSigBits= */ 0L, number); - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java deleted file mode 100644 index fbe936a016..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java +++ /dev/null @@ -1,378 +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; - -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING; -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.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit tests for {@link ReceiverAppStateUpdate}. */ -@RunWith(AndroidJUnit4.class) -public class ReceiverAppStateUpdateTest { - - private static final long MOCK_SEQUENCE_NUMBER = 1; - - @Test - public void statusUpdate_withPlayWhenReady_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setPlayWhenReady(true).build(); - JSONObject stateMessage = createStateMessage().put(KEY_PLAY_WHEN_READY, true); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withPlaybackState_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackState(Player.STATE_BUFFERING) - .build(); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_STATE, STR_STATE_BUFFERING); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withMediaQueue_producesExpectedUpdate() throws JSONException { - HashMap requestHeaders = new HashMap<>(); - requestHeaders.put("key", "value"); - MediaItem.UriBundle media = new MediaItem.UriBundle(Uri.parse("www.media.com"), requestHeaders); - MediaItem.DrmScheme drmScheme1 = - new MediaItem.DrmScheme( - C.WIDEVINE_UUID, - new MediaItem.UriBundle(Uri.parse("www.widevine.com"), requestHeaders)); - MediaItem.DrmScheme drmScheme2 = - new MediaItem.DrmScheme( - C.PLAYREADY_UUID, - new MediaItem.UriBundle(Uri.parse("www.playready.com"), requestHeaders)); - MediaItem item = - new MediaItem.Builder() - .setTitle("title") - .setDescription("description") - .setMedia(media) - .setDrmSchemes(Arrays.asList(drmScheme1, drmScheme2)) - .setStartPositionUs(10) - .setEndPositionUs(20) - .build(); - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setItems(Collections.singletonList(item)) - .build(); - JSONObject object = - createStateMessage() - .put(KEY_MEDIA_QUEUE, new JSONArray().put(ExoCastMessage.mediaItemAsJsonObject(item))); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withRepeatMode_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setRepeatMode(Player.REPEAT_MODE_OFF) - .build(); - JSONObject stateMessage = createStateMessage().put(KEY_REPEAT_MODE, STR_REPEAT_MODE_OFF); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withShuffleModeEnabled_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setShuffleModeEnabled(false).build(); - JSONObject stateMessage = createStateMessage().put(KEY_SHUFFLE_MODE_ENABLED, false); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withIsLoading_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setIsLoading(true).build(); - JSONObject stateMessage = createStateMessage().put(KEY_IS_LOADING, true); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withPlaybackParameters_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackParameters( - new PlaybackParameters( - /* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ false)) - .build(); - JSONObject playbackParamsJson = - new JSONObject().put(KEY_SPEED, .5).put(KEY_PITCH, .25).put(KEY_SKIP_SILENCE, false); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_PARAMETERS, playbackParamsJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withTrackSelectionParameters_producesExpectedUpdate() - throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setTrackSelectionParameters( - TrackSelectionParameters.DEFAULT - .buildUpon() - .setDisabledTextTrackSelectionFlags( - C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT) - .setPreferredAudioLanguage("esp") - .setPreferredTextLanguage("deu") - .setSelectUndeterminedTextLanguage(true) - .build()) - .build(); - - JSONArray selectionFlagsJson = - new JSONArray() - .put(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT) - .put(STR_SELECTION_FLAG_FORCED); - JSONObject playbackParamsJson = - new JSONObject() - .put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, selectionFlagsJson) - .put(KEY_PREFERRED_AUDIO_LANGUAGE, "esp") - .put(KEY_PREFERRED_TEXT_LANGUAGE, "deu") - .put(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, true); - JSONObject object = - createStateMessage().put(KEY_TRACK_SELECTION_PARAMETERS, playbackParamsJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withError_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setErrorMessage("error message") - .build(); - JSONObject stateMessage = createStateMessage().put(KEY_ERROR_MESSAGE, "error message"); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withPlaybackPosition_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackPosition( - new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) - .build(); - JSONObject positionJson = - new JSONObject() - .put(KEY_UUID, new UUID(0, 1)) - .put(KEY_POSITION_MS, 10) - .put(KEY_PERIOD_ID, "period"); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withDiscontinuity_producesExpectedUpdate() throws JSONException { - ReceiverAppStateUpdate stateUpdate = - ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER) - .setPlaybackPosition( - new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build(); - JSONObject positionJson = - new JSONObject() - .put(KEY_UUID, new UUID(0, 1)) - .put(KEY_POSITION_MS, 10) - .put(KEY_PERIOD_ID, "period") - .put(KEY_DISCONTINUITY_REASON, STR_DISCONTINUITY_REASON_SEEK); - JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson); - - assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString())) - .isEqualTo(stateUpdate); - } - - @Test - public void statusUpdate_withMediaItemInfo_producesExpectedTimeline() throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - MediaItem item1 = builder.setUuid(new UUID(0, 1)).build(); - MediaItem item2 = builder.setUuid(new UUID(0, 2)).build(); - - JSONArray periodsJson = new JSONArray(); - periodsJson - .put(new JSONObject().put(KEY_ID, "id1").put(KEY_DURATION_US, 5000000L)) - .put(new JSONObject().put(KEY_ID, "id2").put(KEY_DURATION_US, 7000000L)) - .put(new JSONObject().put(KEY_ID, "id3").put(KEY_DURATION_US, 6000000L)); - JSONObject mediaItemInfoForUuid1 = new JSONObject(); - mediaItemInfoForUuid1 - .put(KEY_WINDOW_DURATION_US, 10000000L) - .put(KEY_DEFAULT_START_POSITION_US, 1000000L) - .put(KEY_PERIODS, periodsJson) - .put(KEY_POSITION_IN_FIRST_PERIOD_US, 2000000L) - .put(KEY_IS_DYNAMIC, false) - .put(KEY_IS_SEEKABLE, true); - JSONObject mediaItemInfoMapJson = - new JSONObject().put(new UUID(0, 1).toString(), mediaItemInfoForUuid1); - - JSONObject receiverAppStateUpdateJson = - createStateMessage().put(KEY_MEDIA_ITEMS_INFO, mediaItemInfoMapJson); - ReceiverAppStateUpdate receiverAppStateUpdate = - ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - Arrays.asList(item1, item2), - receiverAppStateUpdate.mediaItemsInformation, - new ShuffleOrder.DefaultShuffleOrder( - /* shuffledIndices= */ new int[] {1, 0}, /* randomSeed= */ 0)); - Timeline.Window window0 = - timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window(), /* setTag= */ true); - Timeline.Window window1 = - timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window(), /* setTag= */ true); - Timeline.Period[] periods = new Timeline.Period[4]; - for (int i = 0; i < 4; i++) { - periods[i] = - timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true); - } - - assertThat(timeline.getWindowCount()).isEqualTo(2); - assertThat(window0.positionInFirstPeriodUs).isEqualTo(2000000L); - assertThat(window0.durationUs).isEqualTo(10000000L); - assertThat(window0.isDynamic).isFalse(); - assertThat(window0.isSeekable).isTrue(); - assertThat(window0.defaultPositionUs).isEqualTo(1000000L); - assertThat(window1.positionInFirstPeriodUs).isEqualTo(0L); - assertThat(window1.durationUs).isEqualTo(C.TIME_UNSET); - assertThat(window1.isDynamic).isTrue(); - assertThat(window1.isSeekable).isFalse(); - assertThat(window1.defaultPositionUs).isEqualTo(0L); - - assertThat(timeline.getPeriodCount()).isEqualTo(4); - assertThat(periods[0].id).isEqualTo("id1"); - assertThat(periods[0].getPositionInWindowUs()).isEqualTo(-2000000L); - assertThat(periods[0].durationUs).isEqualTo(5000000L); - assertThat(periods[1].id).isEqualTo("id2"); - assertThat(periods[1].durationUs).isEqualTo(7000000L); - assertThat(periods[1].getPositionInWindowUs()).isEqualTo(3000000L); - assertThat(periods[2].id).isEqualTo("id3"); - assertThat(periods[2].durationUs).isEqualTo(6000000L); - assertThat(periods[2].getPositionInWindowUs()).isEqualTo(10000000L); - assertThat(periods[3].durationUs).isEqualTo(C.TIME_UNSET); - } - - @Test - public void statusUpdate_withShuffleOrder_producesExpectedTimeline() throws JSONException { - MediaItem.Builder builder = new MediaItem.Builder(); - JSONObject receiverAppStateUpdateJson = - createStateMessage().put(KEY_SHUFFLE_ORDER, new JSONArray(Arrays.asList(2, 3, 1, 0))); - ReceiverAppStateUpdate receiverAppStateUpdate = - ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString()); - ExoCastTimeline timeline = - ExoCastTimeline.createTimelineFor( - /* mediaItems= */ Arrays.asList( - builder.build(), builder.build(), builder.build(), builder.build()), - /* mediaItemInfoMap= */ Collections.emptyMap(), - /* shuffleOrder= */ new ShuffleOrder.DefaultShuffleOrder( - Util.toArray(receiverAppStateUpdate.shuffleOrder), /* randomSeed= */ 0)); - - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 2, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(3); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 3, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(1); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 1, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(0); - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 0, - /* repeatMode= */ Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(C.INDEX_UNSET); - } - - private static JSONObject createStateMessage() throws JSONException { - return new JSONObject().put(KEY_SEQUENCE_NUMBER, MOCK_SEQUENCE_NUMBER); - } -} From 259bea1652dc44cff0f06d0d865707c12096d4de Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 1 Jul 2019 19:13:03 +0100 Subject: [PATCH 158/807] MediaSessionConnector: Document how to provide metadata asynchronously Issue: #6047 PiperOrigin-RevId: 255992898 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 eaebf8b4e1..3136e3cca9 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 @@ -378,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. */ From 6febc88dce52501d40d5dda2c83289584f8d5a09 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 13:42:05 +0100 Subject: [PATCH 159/807] FLV extractor fixes 1. Only output video starting from a keyframe 2. When calculating the timestamp offset to adjust live streams to start at t=0, use the timestamp of the first tag from which a sample is actually output, rather than just the first audio/video tag. The test streams in the referenced GitHub issue start with a video tag whose packet type is AVC_PACKET_TYPE_SEQUENCE_HEADER (i.e. does not contain a sample) and whose timestamp is set to 0 (i.e. isn't set). The timestamp is set correctly on tags that from which a sample is actually output. Issue: #6111 PiperOrigin-RevId: 256147747 --- RELEASENOTES.md | 2 ++ .../extractor/flv/AudioTagPayloadReader.java | 8 ++++-- .../extractor/flv/FlvExtractor.java | 26 ++++++++++++------- .../extractor/flv/ScriptTagPayloadReader.java | 7 ++--- .../extractor/flv/TagPayloadReader.java | 16 ++++++------ .../extractor/flv/VideoTagPayloadReader.java | 18 ++++++++++--- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 62e985f98b..d76ca54b7b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). ### 2.10.2 ### 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 15b36157fb..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 @@ -73,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; @@ -89,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 @@ -131,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; } @@ -252,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)); @@ -269,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; @@ -291,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..eb1cc8f336 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 @@ -63,7 +63,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 +72,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 +87,7 @@ import java.util.Map; durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } + return false; } private static int readAmfType(ParsableByteArray data) { 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; } } From 7408b4355a990f9a450815ddea9c62945ea9543a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 2 Jul 2019 15:33:50 +0100 Subject: [PATCH 160/807] Add DRM format support checks for MediaSource provided DRM PiperOrigin-RevId: 256161522 --- .../ext/opus/LibopusAudioRenderer.java | 8 +- .../exoplayer2/ext/opus/OpusLibrary.java | 18 ++- .../ext/vp9/LibvpxVideoRenderer.java | 8 +- .../exoplayer2/ext/vp9/VpxLibrary.java | 18 ++- .../com/google/android/exoplayer2/Format.java | 118 ++++++++++++++---- .../audio/MediaCodecAudioRenderer.java | 6 +- .../drm/DefaultDrmSessionManager.java | 6 + .../exoplayer2/drm/DrmSessionManager.java | 14 +++ .../android/exoplayer2/drm/ExoMediaDrm.java | 3 + .../exoplayer2/drm/FrameworkMediaDrm.java | 5 + .../video/MediaCodecVideoRenderer.java | 7 +- .../google/android/exoplayer2/FormatTest.java | 3 +- 12 files changed, 184 insertions(+), 30 deletions(-) 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..c58293dd45 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 @@ -77,12 +77,17 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected int supportsFormatInternal(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; @@ -110,5 +115,4 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT, null, null, 0, null); } - } 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..cd5d67f686 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 @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.ext.opus; 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 +30,7 @@ public final class OpusLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI"); + private static Class exoMediaCryptoType; private OpusLibrary() {} @@ -36,10 +39,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; } /** @@ -56,6 +63,15 @@ public final class OpusLibrary { return isAvailable() ? opusGetVersion() : null; } + /** + * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding + * protected content. + */ + public static boolean matchesExpectedExoMediaCryptoType( + Class exoMediaCryptoType) { + return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType); + } + public static native String opusGetVersion(); public static native boolean opusIsSecureDecodeSupported(); } 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 1734a05e86..b4db4971cc 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 @@ -284,7 +284,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; 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..2c25f674d6 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 @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.ext.vp9; 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 +30,7 @@ public final class VpxLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI"); + private static Class exoMediaCryptoType; private VpxLibrary() {} @@ -36,10 +39,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; } /** @@ -74,6 +81,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( + 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/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index a482022a17..f06c9da048 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; @@ -163,6 +165,15 @@ public final class Format implements Parcelable { */ 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; @@ -236,7 +247,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,7 +352,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Audio. @@ -413,7 +426,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 +532,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Text. @@ -585,7 +600,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } public static Format createTextSampleFormat( @@ -698,7 +714,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } // Image. @@ -740,7 +757,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Generic. @@ -804,7 +822,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 +856,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 +894,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 +931,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 +969,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 +1013,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 +1046,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { @@ -1051,7 +1079,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithContainerInfo( @@ -1099,7 +1128,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } @SuppressWarnings("ReferenceEquality") @@ -1178,7 +1208,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -1210,7 +1241,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithFrameRate(float frameRate) { @@ -1242,7 +1274,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { @@ -1274,7 +1307,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithMetadata(@Nullable Metadata metadata) { @@ -1306,7 +1340,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithRotationDegrees(int rotationDegrees) { @@ -1338,7 +1373,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithBitrate(int bitrate) { @@ -1370,7 +1406,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithVideoSize(int width, int height) { @@ -1402,7 +1439,41 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + 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); } /** @@ -1481,6 +1552,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; @@ -1516,6 +1589,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/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 17591a585e..7e889097bc 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 @@ -308,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) { 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 4e18df04e3..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 @@ -436,6 +436,12 @@ public class DefaultDrmSessionManager return session; } + @Override + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return canAcquireSession(drmInitData) ? mediaDrm.getExoMediaCryptoType() : null; + } + // ProvisioningManager implementation. @Override 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 716da7a0ad..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 @@ -17,6 +17,7 @@ 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; @@ -49,6 +50,12 @@ public interface DrmSessionManager { 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. */ @@ -99,4 +106,11 @@ public interface DrmSessionManager { 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/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 6bd8d9688f..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 @@ -271,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 609abd4e1e..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 @@ -225,6 +225,11 @@ 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/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index c864adfa68..d9d81cf6d4 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 @@ -336,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. 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); From 7964e51e0e46bd89f81c9f4b7ad75221ea24d8a4 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 19:15:08 +0100 Subject: [PATCH 161/807] Remove more classes from nullness blacklist PiperOrigin-RevId: 256202135 --- .../ext/cast/DefaultCastOptionsProvider.java | 3 +- .../exoplayer2/ext/flac/FlacDecoder.java | 2 + .../exoplayer2/ext/flac/FlacDecoderJni.java | 42 +++++++++++-------- extensions/opus/build.gradle | 1 + .../exoplayer2/ext/opus/OpusDecoder.java | 2 + .../exoplayer2/ext/opus/OpusLibrary.java | 8 ++-- .../exoplayer2/ext/vp9/VpxDecoder.java | 6 ++- .../exoplayer2/ext/vp9/VpxLibrary.java | 9 ++-- .../exoplayer2/decoder/SimpleDecoder.java | 3 +- 9 files changed, 47 insertions(+), 29 deletions(-) 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 8948173f60..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; /** @@ -58,7 +59,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider { @Override public List getAdditionalSessionProviders(Context context) { - return null; + return Collections.emptyList(); } } 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..9b15aff846 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,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; @@ -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..a97d99fa54 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,11 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -37,15 +39,16 @@ 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; + @SuppressWarnings("nullness:method.invocation.invalid") public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -58,7 +61,8 @@ import java.nio.ByteBuffer; /** * Sets data to be parsed by libflac. - * @param byteBufferData Source {@link ByteBuffer} + * + * @param byteBufferData Source {@link ByteBuffer}. */ public void setData(ByteBuffer byteBufferData) { this.byteBufferData = byteBufferData; @@ -68,7 +72,8 @@ import java.nio.ByteBuffer; /** * Sets data to be parsed by libflac. - * @param extractorInput Source {@link ExtractorInput} + * + * @param extractorInput Source {@link ExtractorInput}. */ public void setData(ExtractorInput extractorInput) { this.byteBufferData = null; @@ -90,15 +95,15 @@ import java.nio.ByteBuffer; /** * 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. + * + *

        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. */ public int read(ByteBuffer target) throws IOException, InterruptedException { int byteCount = target.remaining(); @@ -106,18 +111,20 @@ import java.nio.ByteBuffer; 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); @@ -234,7 +241,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) { diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 56acbdb7d3..0795079c6b 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,6 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.0.2' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion 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..dbce33b923 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; @@ -150,6 +151,7 @@ import java.util.List; } @Override + @Nullable protected OpusDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { 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 cd5d67f686..5529701c06 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,6 +15,7 @@ */ 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; @@ -30,7 +31,7 @@ public final class OpusLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI"); - private static Class exoMediaCryptoType; + @Nullable private static Class exoMediaCryptoType; private OpusLibrary() {} @@ -56,9 +57,8 @@ 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; } 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..0e13e82630 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,6 +15,7 @@ */ 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; @@ -120,8 +121,9 @@ import java.nio.ByteBuffer; } @Override - protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, - boolean reset) { + @Nullable + protected VpxDecoderException decode( + VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; 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 2c25f674d6..5106ab67ad 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,6 +15,7 @@ */ 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; @@ -30,7 +31,7 @@ public final class VpxLibrary { } private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI"); - private static Class exoMediaCryptoType; + @Nullable private static Class exoMediaCryptoType; private VpxLibrary() {} @@ -56,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; } @@ -67,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; } 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); } From 4c2f211e28b33bd2b60aeb7cd25f8c01f78e4401 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 20:14:38 +0100 Subject: [PATCH 162/807] Remove Flac and Opus renderers from nullness blacklist PiperOrigin-RevId: 256213895 --- .../ext/flac/LibflacAudioRenderer.java | 7 ++-- .../ext/opus/LibopusAudioRenderer.java | 36 ++++++++++++++----- .../exoplayer2/ext/opus/OpusLibrary.java | 2 +- .../exoplayer2/ext/vp9/VpxLibrary.java | 2 +- 4 files changed, 33 insertions(+), 14 deletions(-) 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..376d0fd75e 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,8 +44,8 @@ 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); } 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 c58293dd45..fe74f5c59c 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; @@ -35,10 +36,12 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { /** 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 +51,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,8 +70,11 @@ 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); @@ -106,13 +112,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/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 5529701c06..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 @@ -68,7 +68,7 @@ public final class OpusLibrary { * protected content. */ public static boolean matchesExpectedExoMediaCryptoType( - Class exoMediaCryptoType) { + @Nullable Class exoMediaCryptoType) { return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType); } 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 5106ab67ad..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 @@ -87,7 +87,7 @@ public final class VpxLibrary { * protected content. */ public static boolean matchesExpectedExoMediaCryptoType( - Class exoMediaCryptoType) { + @Nullable Class exoMediaCryptoType) { return Util.areEqual(VpxLibrary.exoMediaCryptoType, exoMediaCryptoType); } From d8c29e82114ad9a7768760c44e5d284bca8716ea Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 3 Jul 2019 09:22:45 +0100 Subject: [PATCH 163/807] Remove unnecessary warning suppression. PiperOrigin-RevId: 256320563 --- .../com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 a97d99fa54..103c2176a0 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 @@ -47,8 +47,7 @@ import java.nio.ByteBuffer; @Nullable private ExtractorInput extractorInput; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - - @SuppressWarnings("nullness:method.invocation.invalid") + public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); From 0145edb60d218a939d869f0dbab0ab7fc0a34477 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 3 Jul 2019 15:07:55 +0100 Subject: [PATCH 164/807] Move MediaSource masking code into separate class. The masking logic for unprepared MediaSources is currently part of ConcatanatingMediaSource. Moving it to its own class nicely separates the code responsibilities and allows reuse. PiperOrigin-RevId: 256360904 --- .../source/ConcatenatingMediaSource.java | 274 ++------------- .../exoplayer2/source/MaskingMediaSource.java | 314 ++++++++++++++++++ .../source/ConcatenatingMediaSourceTest.java | 7 - 3 files changed, 344 insertions(+), 251 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java 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 bdf55fe40d..c72bed1b5b 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 @@ -19,7 +19,6 @@ import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; 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; @@ -71,8 +70,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceByUid; private final boolean isAtomic; private final boolean useLazyPreparation; - private final Timeline.Window window; - private final Timeline.Period period; private boolean timelineUpdateScheduled; private Set nextTimelineUpdateOnCompletionActions; @@ -136,8 +133,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; - window = new Timeline.Window(); - period = new Timeline.Period(); addMediaSources(Arrays.asList(mediaSources)); } @@ -435,33 +430,21 @@ public final 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()) { @@ -728,30 +711,23 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource 0) { MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); newMediaSourceHolder.reset( - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount()); + newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); } - correctOffsets( - newIndex, /* childIndexUpdate= */ 1, newMediaSourceHolder.timeline.getWindowCount()); + 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); } private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { if (mediaSourceHolder == null) { throw new IllegalArgumentException(); } - DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; - if (deferredTimeline.getTimeline() == timeline) { - return; - } if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); int windowOffsetUpdate = @@ -762,61 +738,13 @@ public final 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); - } - } - mediaSourceHolder.isPrepared = true; scheduleTimelineUpdate(); } private void removeMediaSourceInternal(int index) { MediaSourceHolder holder = mediaSourceHolders.remove(index); mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.timeline; + Timeline oldTimeline = holder.mediaSource.getTimeline(); correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); holder.isRemoved = true; maybeReleaseChildSource(holder); @@ -831,7 +759,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; + public final List activeMediaPeriodIds; - public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - 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) { this.childIndex = childIndex; this.firstWindowIndexInChild = firstWindowIndexInChild; - this.hasStartedPreparing = false; - this.isPrepared = false; this.isRemoved = false; - this.activeMediaPeriods.clear(); + this.activeMediaPeriodIds.clear(); } } @@ -944,7 +859,7 @@ public final class ConcatenatingMediaSource 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; + 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; + if (!hasStartedPreparing) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); + unpreparedMaskingMediaPeriod = null; + } + + @Override + public void releaseSourceInternal() { + isPrepared = false; + hasStartedPreparing = false; + super.releaseSourceInternal(); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline newTimeline, @Nullable Object manifest) { + 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; + unpreparedMaskingMediaPeriod = null; + maskingPeriod.overridePreparePositionUs(periodPositionUs); + MediaPeriodId idInSource = + maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); + maskingPeriod.createPeriod(idInSource); + } + } + isPrepared = true; + refreshSourceInfo(this.timeline, manifest); + } + + @Nullable + @Override + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); + } + + 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, + /* 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/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 8137289555..5187addec3 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 @@ -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( From 1060c767fcb491780095a02f670f8e76850a885c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:23:52 +0100 Subject: [PATCH 165/807] Simplify FlacExtractor (step toward enabling nullness checking) - Inline some unnecessarily split out helper methods - Clear ExtractorInput from FlacDecoderJni data after usage - Clean up exception handling for StreamInfo decode failures PiperOrigin-RevId: 256524955 --- .../ext/flac/FlacBinarySearchSeekerTest.java | 4 +- .../ext/flac/FlacExtractorTest.java | 2 +- .../exoplayer2/ext/flac/FlacDecoder.java | 8 +- .../exoplayer2/ext/flac/FlacDecoderJni.java | 37 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 192 ++++++++---------- 5 files changed, 120 insertions(+), 123 deletions(-) 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..934d7cf106 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,7 @@ public final class FlacBinarySearchSeekerTest { FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +70,7 @@ public final class FlacBinarySearchSeekerTest { decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamInfo(), /* 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/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/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 9b15aff846..d20c18e957 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 @@ -17,6 +17,7 @@ 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; @@ -59,14 +60,13 @@ import java.util.List; decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); FlacStreamInfo streamInfo; try { - streamInfo = decoderJni.decodeMetadata(); + streamInfo = decoderJni.decodeStreamInfo(); + } 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; 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 103c2176a0..32ef22dab0 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 @@ -17,6 +17,7 @@ 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.Util; @@ -47,7 +48,7 @@ import java.nio.ByteBuffer; @Nullable private ExtractorInput extractorInput; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - + public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); @@ -59,37 +60,46 @@ import java.nio.ByteBuffer; } /** - * Sets data to be parsed by libflac. + * 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. + * 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; } /** @@ -98,12 +108,11 @@ import java.nio.ByteBuffer; *

        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. 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) { @@ -134,8 +143,12 @@ import java.nio.ByteBuffer; } /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { - return flacDecodeMetadata(nativeDecoderContext); + public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { + FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); + if (streamInfo == null) { + throw new ParserException("Failed to decode StreamInfo"); + } + return streamInfo; } /** 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 79350e6ae3..491b962129 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; @@ -75,22 +75,19 @@ public final class FlacExtractor implements Extractor { private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private final Id3Peeker id3Peeker; - private final boolean isId3MetadataDisabled; + private final boolean id3MetadataDisabled; - private FlacDecoderJni decoderJni; + @Nullable private FlacDecoderJni decoderJni; + @Nullable private ExtractorOutput extractorOutput; + @Nullable private TrackOutput trackOutput; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + private boolean streamInfoDecoded; + @Nullable private FlacStreamInfo streamInfo; + @Nullable private ParsableByteArray outputBuffer; + @Nullable private OutputFrameHolder outputFrameHolder; - private ParsableByteArray outputBuffer; - private ByteBuffer outputByteBuffer; - private BinarySearchSeeker.OutputFrameHolder outputFrameHolder; - private FlacStreamInfo streamInfo; - - private Metadata id3Metadata; - @Nullable private FlacBinarySearchSeeker flacBinarySearchSeeker; - - private boolean readPastStreamInfo; + @Nullable private Metadata id3Metadata; + @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ public FlacExtractor() { @@ -104,7 +101,7 @@ public final class FlacExtractor implements Extractor { */ public FlacExtractor(int flags) { id3Peeker = new Id3Peeker(); - isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @Override @@ -130,48 +127,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(); 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; - } + decodeStreamInfo(input); - writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); - return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return handlePendingSeek(input, seekPosition); + } + + 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()); + 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; + streamInfoDecoded = 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,16 +181,15 @@ 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); } @@ -199,68 +200,61 @@ public final class FlacExtractor implements Extractor { */ private 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) { + private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { + if (streamInfoDecoded) { 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 { + FlacStreamInfo streamInfo; try { - FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); - if (streamInfo == null) { - throw new IOException("Metadata decoding failed"); - } - return streamInfo; + streamInfo = decoderJni.decodeStreamInfo(); } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); + decoderJni.reset(/* newPosition= */ 0); + input.setRetryPosition(/* position= */ 0, e); throw e; } + + streamInfoDecoded = true; + if (this.streamInfo == null) { + this.streamInfo = streamInfo; + outputSeekMap(streamInfo, input.getLength()); + outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata); + outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); + } } - private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { - boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; - SeekMap seekMap = - hasSeekTable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) - : getSeekMapForNonSeekTableFlac(input, streamInfo); + private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + 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); + } + return seekResult; + } + + private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) { + boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + SeekMap seekMap; + if (hasSeekTable) { + seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); + } else if (inputLength != C.LENGTH_UNSET) { + long firstFramePosition = decoderJni.getDecodePosition(); + binarySearchSeeker = + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + seekMap = binarySearchSeeker.getSeekMap(); + } else { + seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); + } extractorOutput.seekMap(seekMap); } - private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { - long inputLength = input.getLength(); - if (inputLength != 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()); - } - } - - private void outputFormat(FlacStreamInfo streamInfo) { + private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -277,25 +271,15 @@ public final class FlacExtractor implements Extractor { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null, - isId3MetadataDisabled ? null : id3Metadata); + metadata); trackOutput.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 void outputSample(ParsableByteArray sampleData, int size, long timeUs) { + sampleData.setPosition(0); + trackOutput.sampleData(sampleData, size); + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ From 948d69b774ec23136431c4142bd82947a35a6981 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:32:20 +0100 Subject: [PATCH 166/807] Make FlacExtractor output methods static This gives a caller greater confidence that the methods have no side effects, and remove any nullness issues with these methods accessing @Nullable member variables. PiperOrigin-RevId: 256525739 --- .../exoplayer2/ext/flac/FlacExtractor.java | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) 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 491b962129..b50554e2f6 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 @@ -33,6 +33,7 @@ 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.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -151,7 +152,7 @@ public final class FlacExtractor implements Extractor { return RESULT_END_OF_INPUT; } - outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp()); + outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput); return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } finally { decoderJni.clearData(); @@ -193,17 +194,6 @@ public final class FlacExtractor implements Extractor { return id3Peeker.peekId3Data(input, id3FramePredicate); } - /** - * 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 { - byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); - return Arrays.equals(header, FLAC_SIGNATURE); - } - private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; @@ -221,8 +211,9 @@ public final class FlacExtractor implements Extractor { streamInfoDecoded = true; if (this.streamInfo == null) { this.streamInfo = streamInfo; - outputSeekMap(streamInfo, input.getLength()); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata); + binarySearchSeeker = + outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); + outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } @@ -230,31 +221,56 @@ public final class FlacExtractor implements Extractor { private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) throws InterruptedException, IOException { + Assertions.checkNotNull(binarySearchSeeker); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs); + outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput); } return seekResult; } - private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) { + /** + * 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 static boolean peekFlacSignature(ExtractorInput input) + throws IOException, InterruptedException { + byte[] header = new byte[FLAC_SIGNATURE.length]; + input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); + return Arrays.equals(header, FLAC_SIGNATURE); + } + + /** + * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to + * handle seeks. + */ + @Nullable + private static FlacBinarySearchSeeker outputSeekMap( + FlacDecoderJni decoderJni, + FlacStreamInfo streamInfo, + long streamLength, + ExtractorOutput output) { boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); - } else if (inputLength != C.LENGTH_UNSET) { + } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); } - extractorOutput.seekMap(seekMap); + output.seekMap(seekMap); + return binarySearchSeeker; } - private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { + private static void outputFormat( + FlacStreamInfo streamInfo, Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -272,13 +288,14 @@ public final class FlacExtractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, metadata); - trackOutput.format(mediaFormat); + output.format(mediaFormat); } - private void outputSample(ParsableByteArray sampleData, int size, long timeUs) { + private static void outputSample( + ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) { sampleData.setPosition(0); - trackOutput.sampleData(sampleData, size); - trackOutput.sampleMetadata( + output.sampleData(sampleData, size); + output.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } From 0f364dfffc0c9af03316f473edd2ee49b7f20a2b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 11:38:48 +0100 Subject: [PATCH 167/807] Remove FlacExtractor from nullness blacklist PiperOrigin-RevId: 256526365 --- extensions/flac/build.gradle | 1 + .../exoplayer2/ext/flac/FlacExtractor.java | 42 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 06a5888404..10b244cb39 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -40,6 +40,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.0.2' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion testImplementation project(modulePrefix + 'testutils-robolectric') 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 b50554e2f6..082068f34d 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 @@ -43,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. @@ -75,17 +78,17 @@ 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 id3MetadataDisabled; @Nullable private FlacDecoderJni decoderJni; - @Nullable private ExtractorOutput extractorOutput; - @Nullable private TrackOutput trackOutput; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; private boolean streamInfoDecoded; - @Nullable private FlacStreamInfo streamInfo; - @Nullable private ParsableByteArray outputBuffer; - @Nullable private OutputFrameHolder outputFrameHolder; + private @MonotonicNonNull FlacStreamInfo streamInfo; + private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; @@ -101,6 +104,7 @@ 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(); id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @@ -132,12 +136,12 @@ public final class FlacExtractor implements Extractor { id3Metadata = peekId3Data(input); } - decoderJni.setData(input); + FlacDecoderJni decoderJni = initDecoderJni(input); try { decodeStreamInfo(input); if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); + return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput); } ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; @@ -194,6 +198,17 @@ public final class FlacExtractor implements Extractor { 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({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; @@ -214,14 +229,19 @@ public final class FlacExtractor implements Extractor { binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputBuffer.reset(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } - private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + @RequiresNonNull("binarySearchSeeker") + private int handlePendingSeek( + ExtractorInput input, + PositionHolder seekPosition, + ParsableByteArray outputBuffer, + OutputFrameHolder outputFrameHolder, + TrackOutput trackOutput) throws InterruptedException, IOException { - Assertions.checkNotNull(binarySearchSeeker); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { @@ -270,7 +290,7 @@ public final class FlacExtractor implements Extractor { } private static void outputFormat( - FlacStreamInfo streamInfo, Metadata metadata, TrackOutput output) { + FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, From 81e7b31be187c1ca7de2129d942b62fdff0079d6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 4 Jul 2019 12:54:56 +0100 Subject: [PATCH 168/807] Apply playback parameters in a consistent way. Currently, we sometimes apply new playback parameters directly and sometimes through the list of playbackParameterCheckpoints. Only when using the checkpoints, we also reset the offset and corresponding position for speedup position calculation. However, these offsets need to be changed in all cases to prevent calculation errors during speedup calculation[1]. This change channels all playback parameters changes through the checkpoints to ensure the offsets get updated accordingly. This fixes an issue introduced in https://github.com/google/ExoPlayer/commit/31911ca54a13b0003d6cf902b95c2ed445afa930. [1] - The speed up is calculated using the ratio of input and output bytes in SonicAudioProcessor.scaleDurationForSpeedUp. Whenever we set new playback parameters to the audio processor these two counts are reset. If we don't reset the offsets too, the scaled timestamp can be a large value compared to the input and output bytes causing massive inaccuracies (like the +20 seconds in the linked issue). Issue:#6117 PiperOrigin-RevId: 256533780 --- RELEASENOTES.md | 7 ++- .../exoplayer2/audio/DefaultAudioSink.java | 47 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d76ca54b7b..855b0e94f2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,8 +12,11 @@ checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* Audio: Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Add VR player demo. 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 f982efa9a7..e3f753958e 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) { @@ -857,8 +842,9 @@ public final class DefaultAudioSink implements AudioSink { // 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; @@ -1040,6 +1026,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() From b1790f9dccffd09fa6de91b8b11ee167af314f37 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 4 Jul 2019 14:55:39 +0100 Subject: [PATCH 169/807] Fix IMA test build issue PiperOrigin-RevId: 256545951 --- .../google/android/exoplayer2/ext/ima/FakeAdsRequest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java index 3c34d9b577..7c2c8a6e0b 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java @@ -105,11 +105,6 @@ public final class FakeAdsRequest implements AdsRequest { throw new UnsupportedOperationException(); } - @Override - public void setContinuousPlayback(boolean b) { - throw new UnsupportedOperationException(); - } - @Override public void setContentDuration(float v) { throw new UnsupportedOperationException(); From 924cfac96657c70c1c97cfd906952af62d6a8160 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 20:02:20 +0100 Subject: [PATCH 170/807] Remove more low hanging fruit from nullness blacklist PiperOrigin-RevId: 256573352 --- .../extractor/flv/ScriptTagPayloadReader.java | 22 ++++++++++++++----- .../exoplayer2/extractor/mp4/Track.java | 1 + .../extractor/mp4/TrackEncryptionBox.java | 2 +- .../google/android/exoplayer2/text/Cue.java | 7 +++--- .../text/SimpleSubtitleDecoder.java | 2 ++ .../exoplayer2/text/SubtitleOutputBuffer.java | 9 ++++---- .../exoplayer2/text/pgs/PgsDecoder.java | 5 ++++- .../exoplayer2/text/ssa/SsaDecoder.java | 7 +++--- .../exoplayer2/text/ssa/SsaSubtitle.java | 4 ++-- .../exoplayer2/text/subrip/SubripDecoder.java | 6 +++-- .../text/subrip/SubripSubtitle.java | 4 ++-- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 16 +++++++++----- 12 files changed, 57 insertions(+), 28 deletions(-) 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 eb1cc8f336..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; } @@ -138,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; } @@ -157,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; } @@ -174,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; } @@ -191,6 +202,7 @@ import java.util.Map; return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -208,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/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/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 29facdb210..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 @@ -29,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; /** 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 b34628b922..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 @@ -18,6 +18,7 @@ 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; /** @@ -46,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/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 d701f99d73..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,6 +15,7 @@ */ 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; @@ -50,7 +51,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatTextIndex; public SsaDecoder() { - this(null); + this(/* initializationData= */ null); } /** @@ -59,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; @@ -202,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 cf174283ec..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 @@ -112,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()); @@ -133,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/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 89017a40c0..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 @@ -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) From fbb76243bdd4c910a1bf2ca725d50cb0269033b4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jul 2019 20:02:32 +0100 Subject: [PATCH 171/807] Clean up DRM post requests - Explicitly specify HTTP_METHOD_POST (previously this was implicit as a result of the body data being non-null) - Use null when there's no body data (it's converted to null inside of the DataSpec constructor anyway) PiperOrigin-RevId: 256573384 --- .../android/exoplayer2/drm/HttpMediaDrmCallback.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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, From d66f0c51a4e6789430cff22194996d2a859b5900 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Jul 2019 16:20:17 +0100 Subject: [PATCH 172/807] CEA608: no-op readability clean-up PiperOrigin-RevId: 256676196 --- .../exoplayer2/text/cea/Cea608Decoder.java | 87 +++++++++++-------- 1 file changed, 49 insertions(+), 38 deletions(-) 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..9d4b914d76 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 @@ -387,45 +387,27 @@ public final class Cea608Decoder extends CeaDecoder { 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) { + if (!updateAndVerifyCurrentChannel(ccData1)) { + // Wrong channel. + continue; + } + + if (isCtrlCode(ccData1)) { + if (isSpecialChar(ccData1, ccData2)) { + // Special North American character. currentCueBuilder.append(getSpecialChar(ccData2)); - } - 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 + } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { + // Extended West European character. + // 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 { + // Non-character control code. + handleCtrl(ccData1, ccData2, repeatedControlPossible); } 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) { @@ -440,8 +422,14 @@ public final class Cea608Decoder extends CeaDecoder { } } + private boolean updateAndVerifyCurrentChannel(byte cc1) { + if (isCtrlCode(cc1)) { + currentChannel = getChannel(cc1); + } + return currentChannel == selectedChannel; + } + private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { - currentChannel = getChannel(cc1); // 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. @@ -459,10 +447,6 @@ public final class Cea608Decoder extends CeaDecoder { } } - if (currentChannel != selectedChannel) { - return; - } - if (isMidrowCtrlCode(cc1, cc2)) { handleMidrowCtrl(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { @@ -681,11 +665,33 @@ public final class Cea608Decoder extends CeaDecoder { return (char) BASIC_CHARACTER_SET[index]; } + private static boolean isSpecialChar(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 getSpecialChar(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 +702,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; From e852ff1dfa1cc1b2339773c47d406c7506507040 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 5 Jul 2019 17:07:38 +0100 Subject: [PATCH 173/807] Add Nullable annotations to CastPlayer PiperOrigin-RevId: 256680382 --- .../exoplayer2/ext/cast/CastPlayer.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) 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 db6f71286e..03518ac18a 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 @@ -83,8 +83,6 @@ 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; @@ -93,9 +91,10 @@ public final class CastPlayer extends BasePlayer { private final CopyOnWriteArrayList listeners; private final ArrayList notificationsBatch; private final ArrayDeque ongoingNotificationsTasks; - private SessionAvailabilityListener sessionAvailabilityListener; + @Nullable private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. + @Nullable private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; @@ -148,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); } @@ -163,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; @@ -180,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); } @@ -194,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)) { @@ -211,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); @@ -229,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) { @@ -246,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 @@ -264,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; } @@ -323,6 +329,7 @@ public final class CastPlayer extends BasePlayer { } @Override + @Nullable public ExoPlaybackException getPlaybackError() { return null; } @@ -530,7 +537,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; @@ -676,7 +683,8 @@ public final class CastPlayer extends BasePlayer { } } - private @Nullable MediaStatus getMediaStatus() { + @Nullable + private MediaStatus getMediaStatus() { return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; } From ecd88c71d2ee5beb08a553091a418fb2e442b87b Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 6 Jul 2019 11:09:36 +0100 Subject: [PATCH 174/807] Remove some UI classes from nullness blacklist PiperOrigin-RevId: 256751627 --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 14 ++++++++------ .../android/exoplayer2/ui/PlayerControlView.java | 11 +++++++---- .../android/exoplayer2/ui/SubtitleView.java | 15 ++++++++++----- 3 files changed, 25 insertions(+), 15 deletions(-) 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/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index b9b2456722..bba422e488 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 @@ -281,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) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 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; 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); From e3af045adbc7c385ad1ce6a97bc35befc3e009c2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Jul 2019 17:24:18 +0100 Subject: [PATCH 175/807] CEA608: Fix repeated Special North American chars. We currently handle most the control code logic after handling special characters. This includes filtering out repeated control codes and checking for the correct channel. As the special character sets are control codes as well, these checks should happen before parsing the characters. Issue:#6133 PiperOrigin-RevId: 256993672 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 91 +++++++++---------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 855b0e94f2..fd748d45ab 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. * FLV: Fix bug that caused playback of some live streams to not start ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* CEA608: Fix repetition of special North American characters + ([#6133](https://github.com/google/ExoPlayer/issues/6133)). ### 2.10.2 ### 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 9d4b914d76..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,15 +378,6 @@ 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. @@ -393,26 +390,29 @@ public final class Cea608Decoder extends CeaDecoder { } if (isCtrlCode(ccData1)) { - if (isSpecialChar(ccData1, ccData2)) { - // Special North American character. - currentCueBuilder.append(getSpecialChar(ccData2)); + if (isSpecialNorthAmericanChar(ccData1, ccData2)) { + currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2)); } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { - // Extended West European character. // Remove standard equivalent of the special extended char before appending new one. currentCueBuilder.backspace(); currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); - } else { - // Non-character control code. - handleCtrl(ccData1, ccData2, repeatedControlPossible); + } 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; - } - - // Basic North American character set. - currentCueBuilder.append(getChar(ccData1)); - if ((ccData2 & 0xE0) != 0x00) { - currentCueBuilder.append(getChar(ccData2)); } + captionDataProcessed = true; } if (captionDataProcessed) { @@ -429,14 +429,15 @@ public final class Cea608Decoder extends CeaDecoder { return currentChannel == selectedChannel; } - private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { + 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 @@ -445,17 +446,11 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } + } else { + // This command is not repeatable. + repeatableControlSet = false; } - - 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) { @@ -660,18 +655,18 @@ 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 boolean isSpecialChar(byte cc1, byte cc2) { + 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 getSpecialChar(byte ccData) { + private static char getSpecialNorthAmericanChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } From 92fb654ab6c3ee3af1ef7992b36f10e23e7733a4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 Jul 2019 10:11:26 +0100 Subject: [PATCH 176/807] Update Robolectric to stable version 4.3. We currently use an alpha version which allowed us to access new threading features. The stable version of this has been released now and we can switch back. PiperOrigin-RevId: 257149681 --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index 3fe22a2762..d9770415f9 100644 --- a/constants.gradle +++ b/constants.gradle @@ -20,7 +20,7 @@ project.ext { compileSdkVersion = 28 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' - robolectricVersion = '4.3-alpha-2' + robolectricVersion = '4.3' autoValueVersion = '1.6' autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' From b3d258b6cf5953acf643688390157b1efae805a2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jul 2019 11:18:50 +0100 Subject: [PATCH 177/807] Fix race condition in DownloadHelper Sending MESSAGE_PREPARE_SOURCE should happen last in the constructor. It was previously happening before initialization finished (and in particular before pendingMediaPeriods was instantiated). Issue: #6146 PiperOrigin-RevId: 257158275 --- .../google/android/exoplayer2/offline/DownloadHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 e7cf87ed6e..4858eec6b7 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 @@ -809,10 +809,10 @@ 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; @@ -824,6 +824,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 +832,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() { From 65d9c11027a1321eb3d59aabfc93e10b0750822d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jul 2019 11:50:56 +0100 Subject: [PATCH 178/807] Bump version to 2.10.3 PiperOrigin-RevId: 257161518 --- RELEASENOTES.md | 27 ++++++++++++------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fd748d45ab..5a1afdd559 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,27 +6,34 @@ and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. * Offline: Add `Scheduler` implementation that uses `WorkManager`. -* Display last frame when seeking to end of stream - ([#2568](https://github.com/google/ExoPlayer/issues/2568)). * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, even if they are listed lower in the `MediaCodecList`. -* Audio: - * Fix an issue where not all audio was played out when the configuration - for the underlying track was changing (e.g., at some period transitions). - * Fix an issue where playback speed was applied inaccurately in playlists - ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * Add a workaround for broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Add VR player demo. * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. -* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. -* FLV: Fix bug that caused playback of some live streams to not start - ([#6111](https://github.com/google/ExoPlayer/issues/6111)). + +### 2.10.3 ### + +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). +* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is + attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). * CEA608: Fix repetition of special North American characters ([#6133](https://github.com/google/ExoPlayer/issues/6133)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming + playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). ### 2.10.2 ### diff --git a/constants.gradle b/constants.gradle index d9770415f9..e857d5a812 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.2' - releaseVersionCode = 2010002 + releaseVersion = '2.10.3' + releaseVersionCode = 2010003 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 db3f3943e1..190f4de5a6 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.2"; + public static final String VERSION = "2.10.3"; /** 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.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.3"; /** * 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 = 2010002; + public static final int VERSION_INT = 2010003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 77e1e4cc1e3a90219bc1f1e128f4d74c7f7a9700 Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Tue, 9 Jul 2019 12:17:54 +0530 Subject: [PATCH 179/807] Add vorbis comments support to flac extractor Decode and add vorbis comments from the flac file to metadata. #5527 --- .../exoplayer2/ext/flac/FlacDecoderJni.java | 10 ++ .../exoplayer2/ext/flac/FlacExtractor.java | 23 +++- extensions/flac/src/main/jni/flac_jni.cc | 26 +++++ extensions/flac/src/main/jni/flac_parser.cc | 28 +++++ .../flac/src/main/jni/include/flac_parser.h | 14 +++ .../metadata/vorbis/VorbisCommentDecoder.java | 59 ++++++++++ .../metadata/vorbis/VorbisCommentFrame.java | 102 ++++++++++++++++++ .../vorbis/VorbisCommentDecoderTest.java | 90 ++++++++++++++++ .../vorbis/VorbisCommentFrameTest.java | 43 ++++++++ 9 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java 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 32ef22dab0..448e2a1b05 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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -151,6 +152,12 @@ import java.nio.ByteBuffer; return streamInfo; } + /** Decodes and consumes the Vorbis Comment section from the FLAC stream. */ + @Nullable + public ArrayList decodeVorbisComment() throws IOException, InterruptedException { + return flacDecodeVorbisComment(nativeDecoderContext); + } + /** * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO * error occurs, resets the stream and input to the given {@code retryPosition}. @@ -269,6 +276,9 @@ import java.nio.ByteBuffer; private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; + private native ArrayList flacDecodeVorbisComment(long context) + throws IOException, InterruptedException; + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; 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 082068f34d..307cdfa8c8 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 @@ -33,6 +33,7 @@ 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.metadata.vorbis.VorbisCommentDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -42,6 +43,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -91,6 +93,7 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; + @Nullable private Metadata vorbisMetadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ @@ -224,11 +227,16 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; + vorbisMetadata = decodeVorbisComment(input); if (this.streamInfo == null) { this.streamInfo = streamInfo; binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); + Metadata metadata = id3MetadataDisabled ? null : id3Metadata; + if (vorbisMetadata != null) { + metadata = vorbisMetadata.copyWithAppendedEntriesFrom(metadata); + } + outputFormat(streamInfo, metadata, trackOutput); outputBuffer.reset(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } @@ -262,6 +270,19 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } + @Nullable + private Metadata decodeVorbisComment(ExtractorInput input) + throws InterruptedException, IOException { + try { + ArrayList vorbisCommentList = decoderJni.decodeVorbisComment(); + return new VorbisCommentDecoder().decodeVorbisComments(vorbisCommentList); + } catch (IOException e) { + decoderJni.reset(0); + input.setRetryPosition(0, e); + throw e; + } + } + /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 298719d48d..0971ba5883 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -110,6 +110,32 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { streamInfo.total_samples); } +DECODER_FUNC(jobject, flacDecodeVorbisComment, jlong jContext) { + Context *context = reinterpret_cast(jContext); + context->source->setFlacDecoderJni(env, thiz); + + VorbisComment vorbisComment = context->parser->getVorbisComment(); + + if (vorbisComment.numComments == 0) { + return NULL; + } else { + jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); + + jmethodID java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + + jobject result = env->NewObject(java_util_ArrayList, java_util_ArrayList_, + vorbisComment.numComments); + for (FLAC__uint32 i = 0; i < vorbisComment.numComments; ++i) { + jstring element = env->NewStringUTF(vorbisComment.metadataArray[i]); + env->CallBooleanMethod(result, java_util_ArrayList_add, element); + env->DeleteLocalRef(element); + } + return result; + } +} + DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { Context *context = reinterpret_cast(jContext); context->source->setFlacDecoderJni(env, thiz); diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 83d3367415..06c98302fd 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -172,6 +172,30 @@ 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 (!mVorbisCommentValid) { + FLAC__uint32 count = 0; + const FLAC__StreamMetadata_VorbisComment *vc = + &metadata->data.vorbis_comment; + mVorbisCommentValid = true; + mVorbisComment.metadataArray = + (char **) malloc(vc->num_comments * sizeof(char *)); + for (FLAC__uint32 i = 0; i < vc->num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry *vce = &vc->comments[i]; + if (vce->entry != NULL) { + mVorbisComment.metadataArray[count] = + (char *) malloc((vce->length + 1) * sizeof(char)); + memcpy(mVorbisComment.metadataArray[count], vce->entry, + vce->length); + mVorbisComment.metadataArray[count][vce->length] = '\0'; + count++; + } + } + mVorbisComment.numComments = count; + } else { + ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); + } + break; default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -233,6 +257,7 @@ FLACParser::FLACParser(DataSource *source) mCurrentPos(0LL), mEOF(false), mStreamInfoValid(false), + mVorbisCommentValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -240,6 +265,7 @@ FLACParser::FLACParser(DataSource *source) ALOGV("FLACParser::FLACParser"); memset(&mStreamInfo, 0, sizeof(mStreamInfo)); memset(&mWriteHeader, 0, sizeof(mWriteHeader)); + memset(&mVorbisComment, 0, sizeof(mVorbisComment)); } FLACParser::~FLACParser() { @@ -266,6 +292,8 @@ 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__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..aec07d673e 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -26,6 +26,11 @@ typedef int status_t; +typedef struct VorbisComment_ { + int numComments; + char **metadataArray; +} VorbisComment; + class FLACParser { public: FLACParser(DataSource *source); @@ -71,6 +76,7 @@ class FLACParser { mEOF = false; if (newPosition == 0) { mStreamInfoValid = false; + mVorbisCommentValid = false; FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -96,6 +102,10 @@ class FLACParser { FLAC__STREAM_DECODER_END_OF_STREAM; } + VorbisComment getVorbisComment() { + return mVorbisComment; + } + private: DataSource *mDataSource; @@ -116,6 +126,8 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + bool mVorbisCommentValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; @@ -129,6 +141,8 @@ class FLACParser { FLACParser(const FLACParser &); FLACParser &operator=(const FLACParser &); + VorbisComment mVorbisComment; + // FLAC parser callbacks as C++ instance methods FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[], size_t *bytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java new file mode 100644 index 0000000000..a212532685 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java @@ -0,0 +1,59 @@ +/* + * 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.vorbis; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; + +/** Decodes vorbis comments */ +public class VorbisCommentDecoder { + + private static final String SEPARATOR = "="; + + /** + * Decodes an {@link ArrayList} of vorbis comments. + * + * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} + * @return A {@link Metadata} structure with the vorbis comments as its entries. + */ + public Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { + if (metadataStringList == null || metadataStringList.size() == 0) { + return null; + } + + ArrayList vorbisCommentFrames = new ArrayList<>(); + VorbisCommentFrame vorbisCommentFrame; + + for (String commentEntry : metadataStringList) { + String[] keyValue; + + keyValue = commentEntry.split(SEPARATOR); + if (keyValue.length != 2) { + /* Could not parse this comment, no key value pair found */ + continue; + } + vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); + vorbisCommentFrames.add(vorbisCommentFrame); + } + + if (vorbisCommentFrames.size() > 0) { + return new Metadata(vorbisCommentFrames); + } else { + return null; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java new file mode 100644 index 0000000000..2deb5b1127 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java @@ -0,0 +1,102 @@ +/* + * 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.vorbis; + +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; + +/** Base class for Vorbis Comment Frames. */ +public class VorbisCommentFrame implements Metadata.Entry { + + /** The frame key and value */ + public final String key; + + public final String value; + + /** + * @param key The key + * @param value Value corresponding to the key + */ + public VorbisCommentFrame(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisCommentFrame(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return key; + } + + @Override + public int describeContents() { + return 0; + } + + // Parcelable implementation. + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj != null) && (obj.getClass() == this.getClass())) { + if (this == obj) { + return true; + } else { + VorbisCommentFrame compareFrame = (VorbisCommentFrame) obj; + if (this.key.equals(compareFrame.key) && this.value.equals(compareFrame.value)) { + return true; + } + } + } + return false; + } + + @Override + public int hashCode() { + int result = 17; + + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + + return result; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisCommentFrame createFromParcel(Parcel in) { + return new VorbisCommentFrame(in); + } + + @Override + public VorbisCommentFrame[] newArray(int size) { + return new VorbisCommentFrame[size]; + } + }; +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java new file mode 100644 index 0000000000..e2c2bcf021 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -0,0 +1,90 @@ +/* + * 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.vorbis; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisCommentDecoder}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentDecoderTest { + + @Test + public void decode() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("Title=Test"); + commentsList.add("Artist=Test2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(2); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Test"); + commentFrame = (VorbisCommentFrame) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Test2"); + } + + @Test + public void decodeEmptyList() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata).isNull(); + } + + @Test + public void decodeTwoSeparators() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("Title=Test"); + commentsList.add("Artist=Test=2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(1); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Test"); + } + + @Test + public void decodeNoSeparators() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("TitleTest"); + commentsList.add("Artist=Test2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(1); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Test2"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java new file mode 100644 index 0000000000..218de9649d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java @@ -0,0 +1,43 @@ +/* + * 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.vorbis; + +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 VorbisCommentFrame}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentFrameTest { + + @Test + public void testParcelable() { + VorbisCommentFrame vorbisCommentFrameToParcel = new VorbisCommentFrame("key", "value"); + + Parcel parcel = Parcel.obtain(); + vorbisCommentFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + VorbisCommentFrame vorbisCommentFrameFromParcel = + VorbisCommentFrame.CREATOR.createFromParcel(parcel); + assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel); + + parcel.recycle(); + } +} From 54abdfc85b413ae6884efb0122fdfe52bae9c1e1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 9 Jul 2019 15:06:12 +0100 Subject: [PATCH 180/807] Fix syntax error in publish.gradle PiperOrigin-RevId: 257184313 --- publish.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish.gradle b/publish.gradle index 96ec3d2f10..f293673c49 100644 --- a/publish.gradle +++ b/publish.gradle @@ -31,7 +31,7 @@ if (project.ext.has("exoplayerPublishEnabled") task.doLast { task.outputs.files .filter { File file -> - file.path.contains("publications") + file.path.contains("publications") \ && file.name.matches("^pom-.+\\.xml\$") } .forEach { File file -> addLicense(file) } From 877923ce5f0cf3c33465d7558edff911c1e0ee11 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 9 Jul 2019 15:11:11 +0100 Subject: [PATCH 181/807] fix typo in release notes PiperOrigin-RevId: 257185017 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5a1afdd559..e40bfe8d81 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,7 +26,7 @@ * Fix an issue where playback speed was applied inaccurately in playlists ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * UI: Fix `PlayerView` incorrectly consuming touch events if no controller is - attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). + attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). * CEA608: Fix repetition of special North American characters ([#6133](https://github.com/google/ExoPlayer/issues/6133)). * FLV: Fix bug that caused playback of some live streams to not start From fb1f91b2a1b2e50d684e1df503c7962bcf05ce8b Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Wed, 10 Jul 2019 17:23:01 +0530 Subject: [PATCH 182/807] Clean up vorbis comment extraction --- .../exoplayer2/ext/flac/FlacDecoderJni.java | 10 --- .../exoplayer2/ext/flac/FlacExtractor.java | 21 +---- extensions/flac/src/main/jni/flac_jni.cc | 61 +++++++------- extensions/flac/src/main/jni/flac_parser.cc | 25 ++---- .../flac/src/main/jni/include/flac_parser.h | 25 +++--- .../metadata/vorbis/VorbisCommentDecoder.java | 59 -------------- .../metadata/vorbis/VorbisCommentFrame.java | 5 +- .../exoplayer2/util/FlacStreamInfo.java | 81 +++++++++++++++++++ .../vorbis/VorbisCommentDecoderTest.java | 40 ++++----- 9 files changed, 158 insertions(+), 169 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java 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 448e2a1b05..32ef22dab0 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 @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -152,12 +151,6 @@ import java.util.ArrayList; return streamInfo; } - /** Decodes and consumes the Vorbis Comment section from the FLAC stream. */ - @Nullable - public ArrayList decodeVorbisComment() throws IOException, InterruptedException { - return flacDecodeVorbisComment(nativeDecoderContext); - } - /** * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO * error occurs, resets the stream and input to the given {@code retryPosition}. @@ -276,9 +269,6 @@ import java.util.ArrayList; private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; - private native ArrayList flacDecodeVorbisComment(long context) - throws IOException, InterruptedException; - private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; 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 307cdfa8c8..50e0458fd7 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 @@ -33,7 +33,6 @@ 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.metadata.vorbis.VorbisCommentDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,7 +42,6 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -93,7 +91,6 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; - @Nullable private Metadata vorbisMetadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ @@ -227,14 +224,13 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; - vorbisMetadata = decodeVorbisComment(input); if (this.streamInfo == null) { this.streamInfo = streamInfo; binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (vorbisMetadata != null) { - metadata = vorbisMetadata.copyWithAppendedEntriesFrom(metadata); + if (streamInfo.vorbisComments != null) { + metadata = streamInfo.vorbisComments.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamInfo, metadata, trackOutput); outputBuffer.reset(streamInfo.maxDecodedFrameSize()); @@ -270,19 +266,6 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } - @Nullable - private Metadata decodeVorbisComment(ExtractorInput input) - throws InterruptedException, IOException { - try { - ArrayList vorbisCommentList = decoderJni.decodeVorbisComment(); - return new VorbisCommentDecoder().decodeVorbisComments(vorbisCommentList); - } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); - throw e; - } - } - /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 0971ba5883..600f181890 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include "include/flac_parser.h" #define LOG_TAG "flac_jni" @@ -90,50 +91,48 @@ DECODER_FUNC(jlong, flacInit) { DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { Context *context = reinterpret_cast(jContext); + jobject commentArrayList = NULL; context->source->setFlacDecoderJni(env, thiz); if (!context->parser->decodeMetadata()) { return NULL; } + bool vorbisCommentValid = context->parser->isVorbisCommentValid(); + + if (vorbisCommentValid) { + std::vector vorbisComments = + context->parser->getVorbisComments(); + + jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); + jmethodID java_util_ArrayList_ = + env->GetMethodID(java_util_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = + env->GetMethodID(java_util_ArrayList, "add", "(Ljava/lang/Object;)Z"); + commentArrayList = env->NewObject(java_util_ArrayList, java_util_ArrayList_, + vorbisComments.size()); + + for (std::vector::const_iterator comment = vorbisComments.begin(); + comment != vorbisComments.end(); ++comment) { + jstring element = env->NewStringUTF((*comment).c_str()); + env->CallBooleanMethod(commentArrayList, java_util_ArrayList_add, + element); + env->DeleteLocalRef(element); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); - jclass cls = env->FindClass( - "com/google/android/exoplayer2/util/" - "FlacStreamInfo"); - jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V"); + jclass cls = env->FindClass("com/google/android/exoplayer2/util/" + "FlacStreamInfo"); + jmethodID constructor = env->GetMethodID(cls, "", + "(IIIIIIIJLjava/util/ArrayList;)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); -} - -DECODER_FUNC(jobject, flacDecodeVorbisComment, jlong jContext) { - Context *context = reinterpret_cast(jContext); - context->source->setFlacDecoderJni(env, thiz); - - VorbisComment vorbisComment = context->parser->getVorbisComment(); - - if (vorbisComment.numComments == 0) { - return NULL; - } else { - jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); - - jmethodID java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList, "", "(I)V"); - jmethodID java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList, "add", - "(Ljava/lang/Object;)Z"); - - jobject result = env->NewObject(java_util_ArrayList, java_util_ArrayList_, - vorbisComment.numComments); - for (FLAC__uint32 i = 0; i < vorbisComment.numComments; ++i) { - jstring element = env->NewStringUTF(vorbisComment.metadataArray[i]); - env->CallBooleanMethod(result, java_util_ArrayList_add, element); - env->DeleteLocalRef(element); - } - return result; - } + streamInfo.total_samples, commentArrayList); } 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 06c98302fd..9af7ec5c8a 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -174,24 +174,16 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { break; case FLAC__METADATA_TYPE_VORBIS_COMMENT: if (!mVorbisCommentValid) { - FLAC__uint32 count = 0; - const FLAC__StreamMetadata_VorbisComment *vc = - &metadata->data.vorbis_comment; - mVorbisCommentValid = true; - mVorbisComment.metadataArray = - (char **) malloc(vc->num_comments * sizeof(char *)); - for (FLAC__uint32 i = 0; i < vc->num_comments; ++i) { - FLAC__StreamMetadata_VorbisComment_Entry *vce = &vc->comments[i]; - if (vce->entry != NULL) { - mVorbisComment.metadataArray[count] = - (char *) malloc((vce->length + 1) * sizeof(char)); - memcpy(mVorbisComment.metadataArray[count], vce->entry, - vce->length); - mVorbisComment.metadataArray[count][vce->length] = '\0'; - count++; + FLAC__StreamMetadata_VorbisComment vc = metadata->data.vorbis_comment; + for (FLAC__uint32 i = 0; i < vc.num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry vce = vc.comments[i]; + if (vce.entry != NULL) { + std::string comment(reinterpret_cast(vce.entry), + vce.length); + mVorbisComments.push_back(comment); } } - mVorbisComment.numComments = count; + mVorbisCommentValid = true; } else { ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); } @@ -265,7 +257,6 @@ FLACParser::FLACParser(DataSource *source) ALOGV("FLACParser::FLACParser"); memset(&mStreamInfo, 0, sizeof(mStreamInfo)); memset(&mWriteHeader, 0, sizeof(mWriteHeader)); - memset(&mVorbisComment, 0, sizeof(mVorbisComment)); } FLACParser::~FLACParser() { diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index aec07d673e..8b70fea4cb 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -18,6 +18,9 @@ #define FLAC_PARSER_H_ #include +#include +#include +#include // libFLAC parser #include "FLAC/stream_decoder.h" @@ -26,11 +29,6 @@ typedef int status_t; -typedef struct VorbisComment_ { - int numComments; - char **metadataArray; -} VorbisComment; - class FLACParser { public: FLACParser(DataSource *source); @@ -49,6 +47,14 @@ class FLACParser { return mStreamInfo; } + bool isVorbisCommentValid() { + return mVorbisCommentValid; + } + + std::vector getVorbisComments() { + return mVorbisComments; + } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -77,6 +83,7 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentValid = false; + mVorbisComments.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -102,10 +109,6 @@ class FLACParser { FLAC__STREAM_DECODER_END_OF_STREAM; } - VorbisComment getVorbisComment() { - return mVorbisComment; - } - private: DataSource *mDataSource; @@ -126,6 +129,8 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + // cached when the VORBIS_COMMENT metadata is parsed by libFLAC + std::vector mVorbisComments; bool mVorbisCommentValid; // cached when a decoded PCM block is "written" by libFLAC parser @@ -141,8 +146,6 @@ class FLACParser { FLACParser(const FLACParser &); FLACParser &operator=(const FLACParser &); - VorbisComment mVorbisComment; - // FLAC parser callbacks as C++ instance methods FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[], size_t *bytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java deleted file mode 100644 index a212532685..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java +++ /dev/null @@ -1,59 +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.metadata.vorbis; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.metadata.Metadata; -import java.util.ArrayList; - -/** Decodes vorbis comments */ -public class VorbisCommentDecoder { - - private static final String SEPARATOR = "="; - - /** - * Decodes an {@link ArrayList} of vorbis comments. - * - * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} - * @return A {@link Metadata} structure with the vorbis comments as its entries. - */ - public Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { - if (metadataStringList == null || metadataStringList.size() == 0) { - return null; - } - - ArrayList vorbisCommentFrames = new ArrayList<>(); - VorbisCommentFrame vorbisCommentFrame; - - for (String commentEntry : metadataStringList) { - String[] keyValue; - - keyValue = commentEntry.split(SEPARATOR); - if (keyValue.length != 2) { - /* Could not parse this comment, no key value pair found */ - continue; - } - vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); - vorbisCommentFrames.add(vorbisCommentFrame); - } - - if (vorbisCommentFrames.size() > 0) { - return new Metadata(vorbisCommentFrames); - } else { - return null; - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java index 2deb5b1127..16bf333902 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java @@ -23,11 +23,12 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; /** Base class for Vorbis Comment Frames. */ -public class VorbisCommentFrame implements Metadata.Entry { +public final class VorbisCommentFrame implements Metadata.Entry { - /** The frame key and value */ + /** The key for this vorbis comment */ public final String key; + /** The value corresponding to this vorbis comment's key */ public final String value; /** 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/FlacStreamInfo.java index 0df39e103d..2d70402bdb 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/FlacStreamInfo.java @@ -15,7 +15,11 @@ */ 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.vorbis.VorbisCommentFrame; +import java.util.ArrayList; /** * Holder for FLAC stream info. @@ -30,6 +34,10 @@ public final class FlacStreamInfo { public final int channels; public final int bitsPerSample; public final long totalSamples; + @Nullable + public final Metadata vorbisComments; + + private static final String SEPARATOR="="; /** * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. @@ -52,6 +60,7 @@ public final class FlacStreamInfo { this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); // Remaining 16 bytes is md5 value + this.vorbisComments = null; } /** @@ -85,6 +94,78 @@ public final class FlacStreamInfo { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; + this.vorbisComments = 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. + * @param maxFrameSize Maximum frame size of the FLAC stream. + * @param sampleRate Sample rate of the FLAC stream. + * @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 vorbisCommentList An {@link ArrayList} that contains vorbis comments, which will + * be converted and stored as metadata in {@link FlacStreamInfo#vorbisComments} + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + */ + public FlacStreamInfo( + int minBlockSize, + int maxBlockSize, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples, + ArrayList vorbisCommentList) { + this.minBlockSize = minBlockSize; + this.maxBlockSize = maxBlockSize; + this.minFrameSize = minFrameSize; + this.maxFrameSize = maxFrameSize; + this.sampleRate = sampleRate; + this.channels = channels; + this.bitsPerSample = bitsPerSample; + this.totalSamples = totalSamples; + this.vorbisComments = decodeVorbisComments(vorbisCommentList); + } + + /** + * Decodes an {@link ArrayList} of vorbis comments. + * + * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} + * @return A {@link Metadata} structure with the vorbis comments as its entries. + */ + @Nullable + private static Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { + if (metadataStringList == null || metadataStringList.isEmpty()) { + return null; + } + + ArrayList vorbisCommentFrames = new ArrayList<>(); + VorbisCommentFrame vorbisCommentFrame; + + for (String commentEntry : metadataStringList) { + String[] keyValue; + + keyValue = commentEntry.split(SEPARATOR, 2); + if (keyValue.length != 2) { + /* Could not parse this comment, no key value pair found */ + continue; + } + vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); + vorbisCommentFrames.add(vorbisCommentFrame); + } + + if (vorbisCommentFrames.isEmpty()) { + return null; + } else { + return new Metadata(vorbisCommentFrames); + } } /** Returns the maximum size for a decoded frame from the FLAC stream. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java index e2c2bcf021..11b373327b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -19,72 +19,72 @@ 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.util.FlacStreamInfo; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for {@link VorbisCommentDecoder}. */ +/** Test for {@link FlacStreamInfo}'s conversion of {@link ArrayList} to {@link Metadata}. */ @RunWith(AndroidJUnit4.class) public final class VorbisCommentDecoderTest { @Test public void decode() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - commentsList.add("Title=Test"); - commentsList.add("Artist=Test2"); + commentsList.add("Title=Song"); + commentsList.add("Artist=Singer"); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); assertThat(commentFrame.key).isEqualTo("Title"); - assertThat(commentFrame.value).isEqualTo("Test"); + assertThat(commentFrame.value).isEqualTo("Song"); commentFrame = (VorbisCommentFrame) metadata.get(1); assertThat(commentFrame.key).isEqualTo("Artist"); - assertThat(commentFrame.value).isEqualTo("Test2"); + assertThat(commentFrame.value).isEqualTo("Singer"); } @Test public void decodeEmptyList() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata).isNull(); } @Test public void decodeTwoSeparators() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - commentsList.add("Title=Test"); - commentsList.add("Artist=Test=2"); + commentsList.add("Title=Song"); + commentsList.add("Artist=Sing=er"); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; - assertThat(metadata.length()).isEqualTo(1); + assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); assertThat(commentFrame.key).isEqualTo("Title"); - assertThat(commentFrame.value).isEqualTo("Test"); + assertThat(commentFrame.value).isEqualTo("Song"); + commentFrame = (VorbisCommentFrame) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Sing=er"); } @Test public void decodeNoSeparators() { - VorbisCommentDecoder decoder = new VorbisCommentDecoder(); ArrayList commentsList = new ArrayList<>(); - commentsList.add("TitleTest"); - commentsList.add("Artist=Test2"); + commentsList.add("TitleSong"); + commentsList.add("Artist=Singer"); - Metadata metadata = decoder.decodeVorbisComments(commentsList); + Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(1); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); assertThat(commentFrame.key).isEqualTo("Artist"); - assertThat(commentFrame.value).isEqualTo("Test2"); + assertThat(commentFrame.value).isEqualTo("Singer"); } } From 4b776ffe4264a93be7266c50fecc03f87db4d18b Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Wed, 10 Jul 2019 20:41:18 +0530 Subject: [PATCH 183/807] Refactor FlacStreamInfo to FlacStreamMetadata --- extensions/flac/proguard-rules.txt | 2 +- .../ext/flac/FlacBinarySearchSeekerTest.java | 4 +- .../ext/flac/FlacBinarySearchSeeker.java | 22 +++++----- .../exoplayer2/ext/flac/FlacDecoder.java | 10 ++--- .../exoplayer2/ext/flac/FlacDecoderJni.java | 14 +++--- .../exoplayer2/ext/flac/FlacExtractor.java | 44 +++++++++---------- extensions/flac/src/main/jni/flac_jni.cc | 2 +- .../exoplayer2/extractor/ogg/FlacReader.java | 28 ++++++++---- ...treamInfo.java => FlacStreamMetadata.java} | 16 +++---- .../vorbis/VorbisCommentDecoderTest.java | 12 ++--- 10 files changed, 82 insertions(+), 72 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/util/{FlacStreamInfo.java => FlacStreamMetadata.java} (94%) diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index ee0a9fa5b5..b44dab3445 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -9,6 +9,6 @@ -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 { *; } 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 934d7cf106..b469a92cb4 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,7 @@ public final class FlacBinarySearchSeekerTest { FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +70,7 @@ public final class FlacBinarySearchSeekerTest { decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeStreamInfo(), /* 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/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 d20c18e957..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 @@ -21,7 +21,7 @@ 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; @@ -58,9 +58,9 @@ import java.util.List; } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeStreamInfo(); + streamMetadata = decoderJni.decodeStreamMetadata(); } catch (ParserException e) { throw new FlacDecoderException("Failed to decode StreamInfo", e); } catch (IOException | InterruptedException e) { @@ -69,9 +69,9 @@ import java.util.List; } 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 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 32ef22dab0..bf9dff9e52 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 @@ -19,7 +19,7 @@ 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; @@ -142,13 +142,13 @@ import java.nio.ByteBuffer; return byteCount; } - /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { - FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); - if (streamInfo == null) { + /** 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 StreamInfo"); } - return streamInfo; + return streamMetadata; } /** @@ -266,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 50e0458fd7..5061cb614f 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 @@ -34,7 +34,7 @@ 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.Assertions; -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 java.io.IOException; @@ -87,7 +87,7 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull TrackOutput trackOutput; private boolean streamInfoDecoded; - private @MonotonicNonNull FlacStreamInfo streamInfo; + private @MonotonicNonNull FlacStreamMetadata streamMetadata; private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; @@ -207,16 +207,16 @@ public final class FlacExtractor implements Extractor { } @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. - @EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. + @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures StreamInfo decoded. @SuppressWarnings({"contracts.postcondition.not.satisfied"}) private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { if (streamInfoDecoded) { return; } - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeStreamInfo(); + streamMetadata = decoderJni.decodeStreamMetadata(); } catch (IOException e) { decoderJni.reset(/* newPosition= */ 0); input.setRetryPosition(/* position= */ 0, e); @@ -224,16 +224,16 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; - if (this.streamInfo == null) { - this.streamInfo = streamInfo; + if (this.streamMetadata == null) { + this.streamMetadata = streamMetadata; binarySearchSeeker = - outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); + outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamInfo.vorbisComments != null) { - metadata = streamInfo.vorbisComments.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.vorbisComments != null) { + metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata); } - outputFormat(streamInfo, metadata, trackOutput); - outputBuffer.reset(streamInfo.maxDecodedFrameSize()); + outputFormat(streamMetadata, metadata, trackOutput); + outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } @@ -273,38 +273,38 @@ public final class FlacExtractor implements Extractor { @Nullable private static FlacBinarySearchSeeker outputSeekMap( FlacDecoderJni decoderJni, - FlacStreamInfo streamInfo, + FlacStreamMetadata streamMetadata, long streamLength, ExtractorOutput output) { boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { - seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); + seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); + new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { - seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); + seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); } output.seekMap(seekMap); return binarySearchSeeker; } private static void outputFormat( - FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { + 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, diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 600f181890..22b581489f 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -124,7 +124,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { context->parser->getStreamInfo(); jclass cls = env->FindClass("com/google/android/exoplayer2/util/" - "FlacStreamInfo"); + "FlacStreamMetadata"); jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJLjava/util/ArrayList;)V"); 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..d4c2bbb485 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); @@ -211,7 +221,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/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java similarity index 94% 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 2d70402bdb..abccbd3c03 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 @@ -24,7 +24,7 @@ import java.util.ArrayList; /** * Holder for FLAC stream info. */ -public final class FlacStreamInfo { +public final class FlacStreamMetadata { public final int minBlockSize; public final int maxBlockSize; @@ -40,14 +40,14 @@ public final class FlacStreamInfo { private static final String SEPARATOR="="; /** - * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. + * Constructs a FlacStreamMetadata parsing the given binary FLAC stream info metadata structure. * * @param data An array holding FLAC stream info metadata structure * @param offset Offset of the structure in the array * @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); @@ -64,7 +64,7 @@ public final class FlacStreamInfo { } /** - * Constructs a FlacStreamInfo given the parameters. + * Constructs a FlacStreamMetadata given the parameters. * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. @@ -77,7 +77,7 @@ public final class FlacStreamInfo { * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, @@ -98,7 +98,7 @@ public final class FlacStreamInfo { } /** - * Constructs a FlacStreamInfo given the parameters. + * Constructs a FlacStreamMetadata given the parameters. * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. @@ -109,11 +109,11 @@ public final class FlacStreamInfo { * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. * @param vorbisCommentList An {@link ArrayList} that contains vorbis comments, which will - * be converted and stored as metadata in {@link FlacStreamInfo#vorbisComments} + * be converted and stored as metadata in {@link FlacStreamMetadata#vorbisComments} * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java index 11b373327b..504dce62b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -19,12 +19,12 @@ 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.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; -/** Test for {@link FlacStreamInfo}'s conversion of {@link ArrayList} to {@link Metadata}. */ +/** Test for {@link FlacStreamMetadata}'s conversion of {@link ArrayList} to {@link Metadata}. */ @RunWith(AndroidJUnit4.class) public final class VorbisCommentDecoderTest { @@ -35,7 +35,7 @@ public final class VorbisCommentDecoderTest { commentsList.add("Title=Song"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); @@ -50,7 +50,7 @@ public final class VorbisCommentDecoderTest { public void decodeEmptyList() { ArrayList commentsList = new ArrayList<>(); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata).isNull(); } @@ -62,7 +62,7 @@ public final class VorbisCommentDecoderTest { commentsList.add("Title=Song"); commentsList.add("Artist=Sing=er"); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(2); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); @@ -80,7 +80,7 @@ public final class VorbisCommentDecoderTest { commentsList.add("TitleSong"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamInfo(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; assertThat(metadata.length()).isEqualTo(1); VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); From 29a099cf03236edc6d46a262d64621dfcdac8989 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Fri, 12 Jul 2019 13:28:41 +0200 Subject: [PATCH 184/807] Switch text track score from the score based logic to a comparison based logic similar to the one we use for audio track selection (see AudioTrackScore). --- .../trackselection/DefaultTrackSelector.java | 154 ++++++++++-------- 1 file changed, 84 insertions(+), 70 deletions(-) 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 949bd178ea..511a974a0e 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 @@ -1552,38 +1552,30 @@ 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); - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - case C.TRACK_TYPE_AUDIO: - // Already done. Do nothing. - break; - case C.TRACK_TYPE_TEXT: - Pair textSelection = - selectTextTrack( - mappedTrackInfo.getTrackGroups(i), - rendererFormatSupports[i], - params, - selectedAudioLanguage); - if (textSelection != null && textSelection.second > selectedTextTrackScore) { - 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. - definitions[selectedTextRendererIndex] = null; - } - definitions[i] = textSelection.first; - selectedTextTrackScore = textSelection.second; - selectedTextRendererIndex = i; + // The below behaviour is different from video and audio track selection + // i.e. do not perform a text track pre selection if there are no preferredTextLanguage requested. + if (C.TRACK_TYPE_TEXT == mappedTrackInfo.getRendererType(i) && params.preferredTextLanguage != null) { + Pair textSelection = + selectTextTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + params); + 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. + definitions[selectedTextRendererIndex] = null; } - break; - default: - definitions[i] = - selectOtherTrack( - trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); - break; + TrackSelection.Definition definition = textSelection.first; + definitions[i] = definition; + selectedTextTrackScore = textSelection.second; + selectedTextRendererIndex = i; + } } } @@ -2051,22 +2043,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped * 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 + * 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) + Parameters params) 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]; @@ -2074,39 +2064,8 @@ 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]); + if ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2535,4 +2494,59 @@ public class DefaultTrackSelector extends MappingTrackSelector { } + /** Represents how well an text track matches the selection {@link Parameters}. */ + protected static final class TextTrackScore implements Comparable { + + private final boolean isWithinRendererCapabilities; + private final int preferredLanguageScore; + private final int localeLanguageMatchIndex; + private final int localeLanguageScore; + private final boolean isDefaultSelectionFlag; + + public TextTrackScore(Format format, Parameters parameters, int formatSupport) { + isWithinRendererCapabilities = isSupported(formatSupport, false); + preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + String[] localeLanguages = Util.getSystemLanguageCodes(); + int bestMatchIndex = Integer.MAX_VALUE; + int bestMatchScore = 0; + for (int i = 0; i < localeLanguages.length; i++) { + int score = getFormatLanguageScore(format, localeLanguages[i]); + if (score > 0) { + bestMatchIndex = i; + bestMatchScore = score; + break; + } + } + localeLanguageMatchIndex = bestMatchIndex; + localeLanguageScore = bestMatchScore; + } + + /** + * 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(@NonNull 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.isDefaultSelectionFlag != other.isDefaultSelectionFlag) { + return this.isDefaultSelectionFlag ? 1 : -1; + } + if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) { + return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex); + } + if (this.localeLanguageScore != other.localeLanguageScore) { + return compareInts(this.localeLanguageScore, other.localeLanguageScore); + } + return 0; + } + } } From 49a2e5a5cba54c6f0099d5ba7df91059cc99a833 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 10 Jul 2019 09:52:53 +0100 Subject: [PATCH 185/807] add manifest to Timeline.Window - Remove manifest argument from callbacks of Player.EventListener and SourceInfoRefreshListener. Instead make it accessible through Player.getCurrentManifest() and Timeline.Window.manifest. - Fix all MediaSource implementation to include the manifest in the Timeline instead of passing it to the SourceInfoRefreshListener. - Refactor ExoPlayerTestRunner, FakeTimeline, FakeMediaSource to reflect these changes and make tests pass. PiperOrigin-RevId: 257359662 --- RELEASENOTES.md | 3 + .../exoplayer2/castdemo/PlayerManager.java | 3 +- .../exoplayer2/ext/cast/CastPlayer.java | 8 +- .../exoplayer2/ext/cast/CastTimeline.java | 1 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 3 +- .../exoplayer2/ext/ima/FakePlayer.java | 4 +- .../ext/leanback/LeanbackPlayerAdapter.java | 3 +- .../mediasession/MediaSessionConnector.java | 3 +- .../google/android/exoplayer2/BasePlayer.java | 14 +- .../android/exoplayer2/ExoPlayerImpl.java | 18 +-- .../exoplayer2/ExoPlayerImplInternal.java | 15 +- .../android/exoplayer2/PlaybackInfo.java | 20 +-- .../com/google/android/exoplayer2/Player.java | 45 +++++- .../android/exoplayer2/SimpleExoPlayer.java | 7 - .../google/android/exoplayer2/Timeline.java | 5 + .../analytics/AnalyticsCollector.java | 3 +- .../exoplayer2/offline/DownloadHelper.java | 11 +- .../exoplayer2/source/BaseMediaSource.java | 14 +- .../source/ClippingMediaSource.java | 7 +- .../source/CompositeMediaSource.java | 15 +- .../source/ConcatenatingMediaSource.java | 8 +- .../source/ExtractorMediaSource.java | 5 +- .../exoplayer2/source/LoopingMediaSource.java | 5 +- .../exoplayer2/source/MaskingMediaSource.java | 5 +- .../exoplayer2/source/MediaPeriod.java | 4 +- .../exoplayer2/source/MediaSource.java | 12 +- .../exoplayer2/source/MergingMediaSource.java | 9 +- .../source/ProgressiveMediaSource.java | 7 +- .../exoplayer2/source/SilenceMediaSource.java | 3 +- .../source/SinglePeriodTimeline.java | 20 ++- .../source/SingleSampleMediaSource.java | 5 +- .../exoplayer2/source/ads/AdsMediaSource.java | 14 +- .../android/exoplayer2/ExoPlayerTest.java | 135 ++++++++---------- .../exoplayer2/MediaPeriodQueueTest.java | 1 - .../analytics/AnalyticsCollectorTest.java | 37 ++--- .../offline/DownloadHelperTest.java | 8 +- .../source/ClippingMediaSourceTest.java | 17 ++- .../source/ConcatenatingMediaSourceTest.java | 33 +++-- .../source/LoopingMediaSourceTest.java | 4 +- .../source/MergingMediaSourceTest.java | 5 +- .../source/SinglePeriodTimelineTest.java | 9 +- .../source/dash/DashMediaSource.java | 3 +- .../exoplayer2/source/hls/HlsMediaSource.java | 5 +- .../source/smoothstreaming/SsMediaSource.java | 5 +- .../exoplayer2/ui/PlayerControlView.java | 3 +- .../ui/PlayerNotificationManager.java | 2 +- .../android/exoplayer2/testutil/Action.java | 8 +- .../testutil/ExoPlayerTestRunner.java | 30 +--- .../testutil/FakeAdaptiveMediaSource.java | 3 +- .../exoplayer2/testutil/FakeMediaSource.java | 13 +- .../exoplayer2/testutil/FakeTimeline.java | 19 ++- .../testutil/MediaSourceTestRunner.java | 2 +- .../exoplayer2/testutil/StubExoPlayer.java | 5 - 53 files changed, 316 insertions(+), 330 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e40bfe8d81..59f30b8a0a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,9 @@ * Add VR player demo. * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. +* Do not pass the manifest to callbacks of Player.EventListener and + SourceInfoRefreshListener anymore. Instead make it accessible through + Player.getCurrentManifest() and Timeline.Window.manifest. ### 2.10.3 ### diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index c92ebd7e94..d2a1ca0860 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -264,8 +264,7 @@ import org.json.JSONObject; } @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { updateCurrentItemIndex(); } 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 03518ac18a..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 @@ -460,11 +460,6 @@ public final class CastPlayer extends BasePlayer { return currentTimeline; } - @Override - @Nullable public Object getCurrentManifest() { - return null; - } - @Override public int getCurrentPeriodIndex() { return getCurrentWindowIndex(); @@ -592,8 +587,7 @@ public final class CastPlayer extends BasePlayer { waitingForInitialTimeline = false; notificationsBatch.add( new ListenerNotificationTask( - listener -> - listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason))); + listener -> listener.onTimelineChanged(currentTimeline, reason))); } } 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/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 5a266c290d..249271dc61 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 @@ -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; 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/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 1fece6bc8e..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 @@ -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/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 3136e3cca9..be085ae30b 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 @@ -1020,8 +1020,7 @@ 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(); 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/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 945bd32d30..73107aa98e 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 @@ -547,11 +547,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) { @@ -639,7 +634,6 @@ import java.util.concurrent.CopyOnWriteArrayList; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; return new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, @@ -713,7 +707,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; @@ -737,9 +731,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; @@ -747,12 +739,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 a6d4352880..5f53427fca 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 @@ -267,9 +267,10 @@ import java.util.concurrent.atomic.AtomicBoolean; // MediaSource.SourceInfoRefreshListener 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. @@ -899,7 +900,6 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, @@ -1276,9 +1276,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; @@ -1881,12 +1880,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/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index d3e4a0e626..1eedae08b6 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. */ - @Nullable public final Object manifest; /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; /** @@ -91,7 +88,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 +105,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 +119,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ public PlaybackInfo( Timeline timeline, - @Nullable Object manifest, MediaPeriodId periodId, long startPositionUs, long contentPositionUs, @@ -137,7 +131,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; @@ -187,7 +180,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 +194,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 +226,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithPlaybackState(int playbackState) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -260,7 +249,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithIsLoading(boolean isLoading) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -286,7 +274,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -310,7 +297,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..68a386d2de 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. 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 b427991d6e..a782255cb8 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 @@ -1070,13 +1070,6 @@ public class SimpleExoPlayer extends BasePlayer return player.getCurrentTimeline(); } - @Override - @Nullable - public Object getCurrentManifest() { - verifyApplicationThread(); - return player.getCurrentManifest(); - } - @Override public int getCurrentPeriodIndex() { verifyApplicationThread(); 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 deecfb15a8..de0f177342 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 @@ -437,8 +437,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) { 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 4858eec6b7..17bc304db3 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 @@ -335,6 +335,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; @@ -374,6 +375,7 @@ public final class DownloadHelper { trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); callbackHandler = new Handler(Util.getLooper()); + window = new Timeline.Window(); } /** @@ -409,7 +411,9 @@ public final class DownloadHelper { return null; } assertPreparedWithMedia(); - return mediaPreparer.manifest; + return mediaPreparer.timeline.getWindowCount() > 0 + ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest + : null; } /** @@ -814,7 +818,6 @@ public final class DownloadHelper { private final HandlerThread mediaSourceThread; private final Handler mediaSourceHandler; - @Nullable public Object manifest; public @MonotonicNonNull Timeline timeline; public MediaPeriod @MonotonicNonNull [] mediaPeriods; @@ -892,14 +895,12 @@ public final class DownloadHelper { // MediaSource.SourceInfoRefreshListener 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 = 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..124f70c64c 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 @@ -27,8 +27,8 @@ import java.util.ArrayList; * 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 { @@ -37,7 +37,6 @@ public abstract class BaseMediaSource implements MediaSource { @Nullable private Looper looper; @Nullable private Timeline timeline; - @Nullable private Object manifest; public BaseMediaSource() { sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); @@ -65,13 +64,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); + listener.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -139,7 +136,7 @@ public abstract class BaseMediaSource implements MediaSource { this.looper = looper; prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + listener.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -149,7 +146,6 @@ public abstract class BaseMediaSource implements MediaSource { if (sourceInfoListeners.isEmpty()) { looper = null; timeline = null; - manifest = null; releaseSourceInternal(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index c942f9320e..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 @@ -87,7 +87,6 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final ArrayList mediaPeriods; private final Timeline.Window window; - @Nullable private Object manifest; @Nullable private ClippingTimeline clippingTimeline; @Nullable private IllegalClippingException clippingError; private long periodStartUs; @@ -235,12 +234,10 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @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); } @@ -280,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 1a9e1ff250..612ad33f9d 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 @@ -73,17 +73,15 @@ 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(Object, 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()}. @@ -94,7 +92,12 @@ public abstract class CompositeMediaSource extends BaseMediaSource { protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); SourceInfoRefreshListener sourceListener = - (source, timeline, manifest) -> onChildSourceInfoRefreshed(id, source, timeline, manifest); + new SourceInfoRefreshListener() { + @Override + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + onChildSourceInfoRefreshed(id, source, timeline); + } + }; MediaSourceEventListener eventListener = new ForwardingEventListener(id); childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); 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 c72bed1b5b..18d5c49fb4 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 @@ -474,10 +474,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource onCompletionActions = nextTimelineUpdateOnCompletionActions; nextTimelineUpdateOnCompletionActions = new HashSet<>(); - refreshSourceInfo( - new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic), /* manifest= */ null); + refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic)); getPlaybackThreadHandlerOnPlaybackThread() .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); 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 f07ee63e79..2bcaad4fce 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 @@ -364,9 +364,8 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { - refreshSourceInfo(timeline, manifest); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + refreshSourceInfo(timeline); } @Deprecated 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 7adb18dc94..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 @@ -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/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index ad9ef194da..1fca824910 100644 --- 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 @@ -119,7 +119,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline newTimeline, @Nullable Object manifest) { + Void id, MediaSource mediaSource, Timeline newTimeline) { if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); } else if (newTimeline.isEmpty()) { @@ -162,7 +162,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } } isPrepared = true; - refreshSourceInfo(this.timeline, manifest); + refreshSourceInfo(this.timeline); } @Nullable @@ -274,6 +274,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { return window.set( tag, + /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, /* isSeekable= */ false, 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..f86be8afc2 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 @@ -58,8 +58,8 @@ public interface MediaPeriod extends SequenceableLoader { * *

        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}. + * MediaSource.SourceInfoRefreshListener#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. 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 82359ffccd..10e29f3f44 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 @@ -49,16 +49,16 @@ public interface MediaSource { interface SourceInfoRefreshListener { /** - * Called when manifest and/or timeline has been refreshed. - *

        - * Called on the playback thread. + * Called when the 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); - + default void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + // Do nothing. + } } /** 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 f12ce92f54..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,7 +71,6 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - @Nullable private Object primaryManifest; private int periodCount; @Nullable private IllegalMergeException mergeError; @@ -143,7 +142,6 @@ public final class MergingMediaSource extends CompositeMediaSource { 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,11 +159,8 @@ 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]); } } 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 ba69b46d7f..42ec237b3e 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 @@ -287,7 +287,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/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index fc99e8cb7b..a5b78ef3f7 100644 --- 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 @@ -68,8 +68,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), - /* manifest= */ null); + new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false)); } @Override 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 14648775f8..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 @@ -36,6 +36,7 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isSeekable; private final boolean isDynamic; @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/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 6c1881a01a..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 @@ -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. @@ -304,7 +305,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - refreshSourceInfo(timeline, /* manifest= */ null); + refreshSourceInfo(timeline); } @Override 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 a6c2cf2767..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 @@ -134,7 +134,6 @@ public final class AdsMediaSource extends CompositeMediaSource { // Accessed on the player thread. @Nullable private ComponentListener componentListener; @Nullable private Timeline contentTimeline; - @Nullable private Object contentManifest; @Nullable private AdPlaybackState adPlaybackState; private @NullableType MediaSource[][] adGroupMediaSources; private @NullableType Timeline[][] adGroupTimelines; @@ -265,7 +264,6 @@ public final class AdsMediaSource extends CompositeMediaSource { componentListener = null; maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; - contentManifest = null; adPlaybackState = null; adGroupMediaSources = new MediaSource[0][]; adGroupTimelines = new Timeline[0][]; @@ -274,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); } } @@ -308,10 +303,9 @@ public final class AdsMediaSource extends CompositeMediaSource { maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest) { + private void onContentSourceInfoRefreshed(Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); contentTimeline = timeline; - contentManifest = manifest; maybeUpdateSourceInfo(); } @@ -340,7 +334,7 @@ public final class AdsMediaSource extends CompositeMediaSource { adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); - refreshSourceInfo(timeline, contentManifest); + refreshSourceInfo(timeline); } } 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..61b8418411 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 @@ -113,8 +113,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 +126,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); @@ -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() @@ -1450,8 +1439,7 @@ public final class ExoPlayerTest { @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 +1597,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 +1760,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 +1833,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 +1859,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 +2008,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 +2054,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 +2148,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 +2193,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(); @@ -2274,8 +2259,8 @@ public final class ExoPlayerTest { 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) + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.AUDIO_FORMAT) }; MediaSource mediaSource = new ConcatenatingMediaSource(fakeMediaSources); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -2326,10 +2311,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 +2347,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 +2391,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 +2429,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 +2454,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 +2502,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(); 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..def7f8552e 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 @@ -353,7 +353,6 @@ public final class MediaPeriodQueueTest { playbackInfo = new PlaybackInfo( timeline, - /* manifest= */ null, mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, 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 22aa63b83a..a2546adfe4 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 = @@ -443,10 +437,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,8 +499,7 @@ 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) @@ -570,7 +561,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 = @@ -639,7 +630,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() 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..479936b82f 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 @@ -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); @@ -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 5187addec3..c587d85a85 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 @@ -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 @@ -307,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); @@ -362,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); @@ -387,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 { @@ -660,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); @@ -807,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); @@ -840,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(); @@ -874,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); @@ -1092,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/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/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 551555502f..b9cb901041 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 @@ -994,7 +994,7 @@ public final class DashMediaSource extends BaseMediaSource { windowDefaultStartPositionUs, manifest, tag); - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!sideloadedManifest) { // Remove any pending simulated refresh. @@ -1193,6 +1193,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/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 f891670e78..12c6a8ee72 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 @@ -383,6 +383,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 +404,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 +420,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/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 e31fbccae5..c053f255fc 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 @@ -669,6 +669,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 +691,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 +704,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/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 bba422e488..e408035e98 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 @@ -1212,8 +1212,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 c800c7bd63..9ad951cb17 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 @@ -1286,7 +1286,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/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 93e52bc23a..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 @@ -542,9 +542,7 @@ 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 { @Nullable private final Timeline expectedTimeline; @@ -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/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index cc4b3a60d7..f7c6694409 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 @@ -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; @@ -347,7 +347,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc 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<>(); @@ -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()); 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/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 80456169ff..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,7 +56,6 @@ public class FakeMediaSource extends BaseMediaSource { private final ArrayList createdMediaPeriods; protected Timeline timeline; - private Object manifest; private boolean preparedSource; private boolean releasedSource; private Handler sourceInfoRefreshHandler; @@ -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; @@ -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/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/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 0873dbd145..6d626088fc 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -345,7 +345,7 @@ public class MediaSourceTestRunner { // SourceInfoRefreshListener 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/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index df96b634dd..eaebe5a12d 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -195,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(); From d4e3e8f2e085d0bc74a09b0bc5373af61212da06 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jul 2019 17:38:03 +0100 Subject: [PATCH 186/807] Add overridingDrmInitData to DecryptableSampleQueueReader To use in HLS when session keys are provided PiperOrigin-RevId: 257421156 --- .../source/DecryptableSampleQueueReader.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index 4f0c5b87aa..42453929c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -27,6 +27,8 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -39,6 +41,7 @@ public final class DecryptableSampleQueueReader { private final DrmSessionManager sessionManager; private final FormatHolder formatHolder; private final boolean playClearSamplesWithoutKeys; + private final HashMap overridingDrmInitDatas; private @MonotonicNonNull Format currentFormat; @Nullable private DrmSession currentSession; @@ -55,6 +58,19 @@ public final class DecryptableSampleQueueReader { formatHolder = new FormatHolder(); playClearSamplesWithoutKeys = (sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0; + overridingDrmInitDatas = new HashMap<>(); + } + + /** + * Given a mapping from {@link DrmInitData#schemeType} to {@link DrmInitData}, overrides any + * {@link DrmInitData} read from the upstream {@link SampleQueue} whose {@link + * DrmInitData#schemeType} is a key in the mapping to use the corresponding {@link DrmInitData} + * value. If {@code overridingDrmInitDatas} does not contain a mapping for the upstream {@link + * DrmInitData#schemeType}, the upstream {@link DrmInitData} is used. + */ + public void setOverridingDrmInitDatas(Map overridingDrmInitDatas) { + this.overridingDrmInitDatas.clear(); + this.overridingDrmInitDatas.putAll(overridingDrmInitDatas); } /** Releases any resources acquired by this reader. */ @@ -170,6 +186,10 @@ public final class DecryptableSampleQueueReader { DrmSession previousSession = currentSession; DrmInitData drmInitData = currentFormat.drmInitData; if (drmInitData != null) { + DrmInitData overridingDrmInitData = overridingDrmInitDatas.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } currentSession = sessionManager.acquireSession(Assertions.checkNotNull(Looper.myLooper()), drmInitData); } else { From 6606a4ff010fcade2bd73c0dfad902075fd7f25c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 10 Jul 2019 20:21:42 +0100 Subject: [PATCH 187/807] CronetDataSource: Fix invalid Javadoc tag PiperOrigin-RevId: 257456890 --- .../google/android/exoplayer2/ext/cronet/CronetDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b1f907cc37..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 @@ -554,7 +554,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @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 {@codes buffer} is not a direct ByteBuffer. + * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer. */ public int read(ByteBuffer buffer) throws HttpDataSourceException { Assertions.checkState(opened); From 972c6c2f5c5b164dea996c4dae6ed5d5308fd163 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jul 2019 20:34:33 +0100 Subject: [PATCH 188/807] Avoid acquiring DrmSessions using the dummy DrmSessionManager This is a temporary workaround until we have migrated all MediaSources uses. This change avoids having to migrate all uses of MediaSources immediately. PiperOrigin-RevId: 257459138 --- .../source/DecryptableSampleQueueReader.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index 42453929c4..e33afaee60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -131,7 +131,8 @@ public final class DecryptableSampleQueueReader { if (currentFormat == null || formatRequired) { readFlagFormatRequired = true; - } else if (currentFormat.drmInitData != null + } else if (sessionManager != DrmSessionManager.DUMMY + && currentFormat.drmInitData != null && Assertions.checkNotNull(currentSession).getState() != DrmSession.STATE_OPENED_WITH_KEYS) { if (playClearSamplesWithoutKeys) { @@ -158,12 +159,7 @@ public final class DecryptableSampleQueueReader { if (onlyPropagateFormatChanges && currentFormat == formatHolder.format) { return C.RESULT_NOTHING_READ; } - onFormat(Assertions.checkNotNull(formatHolder.format)); - // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model - // [Internal ref: b/129764794]. - outputFormatHolder.includesDrmSession = true; - outputFormatHolder.format = formatHolder.format; - outputFormatHolder.drmSession = currentSession; + onFormat(Assertions.checkNotNull(formatHolder.format), outputFormatHolder); } return result; } @@ -172,10 +168,21 @@ public final class DecryptableSampleQueueReader { * 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) { - DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; + private void onFormat(Format format, FormatHolder outputFormatHolder) { + outputFormatHolder.format = format; 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; + DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; if (Util.areEqual(oldDrmInitData, format.drmInitData)) { // Nothing to do. return; @@ -195,6 +202,7 @@ public final class DecryptableSampleQueueReader { } else { currentSession = null; } + outputFormatHolder.drmSession = currentSession; if (previousSession != null) { previousSession.releaseReference(); @@ -211,8 +219,9 @@ public final class DecryptableSampleQueueReader { } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_CLEAR) { return currentSession == null || playClearSamplesWithoutKeys; } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED) { - return Assertions.checkNotNull(currentSession).getState() - == DrmSession.STATE_OPENED_WITH_KEYS; + return sessionManager == DrmSessionManager.DUMMY + || Assertions.checkNotNull(currentSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS; } else { throw new IllegalStateException(); } From 6796b179a6a0be7e85a9237bed7806e6d059b489 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jul 2019 22:11:55 +0100 Subject: [PATCH 189/807] Make onInputFormatChanged methods in Renderers take FormatHolders PiperOrigin-RevId: 257478434 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 14 +++++++------- .../audio/SimpleDecoderAudioRenderer.java | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) 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 b4db4971cc..56f5fd2d09 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 @@ -307,7 +307,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()); @@ -491,15 +491,15 @@ public class LibvpxVideoRenderer extends BaseRenderer { /** * 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 @SuppressWarnings("unchecked") - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + 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); @@ -513,7 +513,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); if (sourceDrmSession != null) { sourceDrmSession.releaseReference(); } @@ -826,7 +826,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { 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 93c60f2917..ef0207517a 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 @@ -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()); @@ -438,7 +438,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()) { @@ -656,9 +656,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @SuppressWarnings("unchecked") - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + 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); @@ -673,7 +673,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + drmSessionManager.acquireSession(Looper.myLooper(), inputFormat.drmInitData); if (sourceDrmSession != null) { sourceDrmSession.releaseReference(); } @@ -694,10 +694,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) { From 91750b80098a0c721bbc45158912737517f80c69 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 11:16:54 +0100 Subject: [PATCH 190/807] Plumb DrmSessionManager into DashMediaSource PiperOrigin-RevId: 257576791 --- .../exoplayer2/demo/PlayerActivity.java | 21 +++++++++++---- .../source/DecryptableSampleQueueReader.java | 3 +-- .../source/chunk/ChunkSampleStream.java | 23 ++++++++++------ .../source/dash/DashMediaPeriod.java | 25 +++++++++++++++--- .../source/dash/DashMediaSource.java | 26 +++++++++++++++++++ .../source/dash/DashMediaPeriodTest.java | 2 ++ .../dash/offline/DownloadHelperTest.java | 3 ++- .../source/smoothstreaming/SsMediaPeriod.java | 2 ++ .../testutil/FakeAdaptiveMediaPeriod.java | 2 ++ 9 files changed, 87 insertions(+), 20 deletions(-) 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 929b579b4c..59d861e13d 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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; @@ -358,7 +359,7 @@ public class PlayerActivity extends AppCompatActivity return; } - DefaultDrmSessionManager drmSessionManager = null; + DrmSessionManager drmSessionManager = null; if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA); String[] keyRequestPropertiesArray = @@ -389,6 +390,8 @@ public class PlayerActivity extends AppCompatActivity finish(); return; } + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); } TrackSelection.Factory trackSelectionFactory; @@ -425,7 +428,7 @@ public class PlayerActivity extends AppCompatActivity MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], drmSessionManager); } mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -455,10 +458,16 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource(uri, null); + return buildMediaSource( + uri, + /* overrideExtension= */ null, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager()); } - private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { + private MediaSource buildMediaSource( + Uri uri, + @Nullable String overrideExtension, + DrmSessionManager drmSessionManager) { DownloadRequest downloadRequest = ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri); if (downloadRequest != null) { @@ -467,7 +476,9 @@ public class PlayerActivity extends AppCompatActivity @ContentType int type = Util.inferContentType(uri, overrideExtension); 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); case C.TYPE_HLS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index e33afaee60..b0b10d4e98 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -172,6 +172,7 @@ public final class DecryptableSampleQueueReader { */ 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 @@ -182,12 +183,10 @@ public final class DecryptableSampleQueueReader { } outputFormatHolder.includesDrmSession = true; outputFormatHolder.drmSession = currentSession; - DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; 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; 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 efc3b47596..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,6 +21,9 @@ 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; @@ -71,6 +74,7 @@ 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; @@ -94,6 +98,8 @@ 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 drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ @@ -105,6 +111,7 @@ public class ChunkSampleStream implements SampleStream, S Callback> callback, Allocator allocator, long positionUs, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; @@ -126,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; @@ -328,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(); } @@ -349,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(); } @@ -367,13 +378,9 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } maybeNotifyPrimaryTrackFormatChanged(); - return primarySampleQueue.read( - formatHolder, - buffer, - formatRequired, - /* allowOnlyClearBuffers= */ false, - loadingFinished, - decodeOnlyUntilPositionUs); + + return primarySampleQueueReader.read( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); } @Override 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 be0aa4f154..b34b677d45 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; @@ -70,6 +72,7 @@ import java.util.regex.Pattern; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final TransferListener transferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -97,6 +100,7 @@ import java.util.regex.Pattern; int periodIndex, DashChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffsetMs, @@ -109,6 +113,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 +128,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(); @@ -455,7 +460,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 +482,7 @@ import java.util.regex.Pattern; int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos( + drmSessionManager, adaptationSets, groupedAdaptationSetIndices, primaryGroupCount, @@ -569,6 +577,7 @@ import java.util.regex.Pattern; } private static int buildPrimaryAndEmbeddedTrackGroupInfos( + DrmSessionManager drmSessionManager, List adaptationSets, int[][] groupedAdaptationSetIndices, int primaryGroupCount, @@ -585,7 +594,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 +708,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 b9cb901041..92c47f2f12 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(); @@ -132,6 +136,20 @@ public final class DashMediaSource 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 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, @@ -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; @@ -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, @@ -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 @@ -574,6 +597,7 @@ public final class DashMediaSource extends BaseMediaSource { 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; @@ -660,6 +685,7 @@ public final class DashMediaSource extends BaseMediaSource { periodIndex, chunkSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, periodEventDispatcher, elapsedRealtimeOffsetMs, 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/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java index 0b7c06f813..73225f68c7 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; 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; @@ -37,7 +38,7 @@ public final class DownloadHelperTest { Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], - /* drmSessionManager= */ null, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); } } 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 38781782eb..6d4db3fbbb 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; 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; @@ -237,6 +238,7 @@ import java.util.List; this, allocator, positionUs, + DrmSessionManager.getDummyDrmSessionManager(), loadErrorHandlingPolicy, eventDispatcher); } 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 fea863c48e..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; @@ -149,6 +150,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod /* callback= */ this, allocator, /* positionUs= */ 0, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), eventDispatcher); } From 3f24d4433ae36004672567b8d22cfb0f88fa4c96 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 11:33:38 +0100 Subject: [PATCH 191/807] Optimize DrmSession reference replacement Potentially avoids up to two calls to synchronized methods PiperOrigin-RevId: 257578304 --- .../java/com/google/android/exoplayer2/drm/DrmSession.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 df45323ca3..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 @@ -31,10 +31,15 @@ public interface DrmSession { /** * Invokes {@code newSession's} {@link #acquireReference()} and {@code previousSession's} {@link - * #releaseReference()} in that order. Does nothing for passed null values. + * #releaseReference()} in that order. Null arguments are ignored. Does nothing if {@code + * previousSession} and {@code newSession} are the same session. */ static void replaceSessionReferences( @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (previousSession == newSession) { + // Do nothing. + return; + } if (newSession != null) { newSession.acquireReference(); } From bbcd46e98aac55245f8ec90925dd20e0213f998f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 14:42:51 +0100 Subject: [PATCH 192/807] Change HlsSampleStreamWrapper.prepareWithMasterPlaylistInfo to take a TrackGroup[] Non-functional change. Makes it easier to add the ExoMediaCrypto type information to the formats. PiperOrigin-RevId: 257598282 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 11 +++---- .../source/hls/HlsSampleStreamWrapper.java | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) 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 d834c097cf..39b49da402 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 @@ -487,7 +487,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 +646,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 +704,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); } } } 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 96704053cb..079852c4d4 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 @@ -51,8 +51,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 @@ -122,7 +124,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; @@ -200,18 +202,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 = new TrackGroupArray(trackGroups); + optionalTrackGroups = new HashSet<>(); + for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { + optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); + } this.primaryTrackGroupIndex = primaryTrackGroupIndex; handler.post(callback::onPrepared); } @@ -231,9 +235,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. @@ -1046,7 +1050,7 @@ import java.util.Map; } this.trackGroups = new TrackGroupArray(trackGroups); Assertions.checkState(optionalTrackGroups == null); - optionalTrackGroups = TrackGroupArray.EMPTY; + optionalTrackGroups = Collections.emptySet(); } private HlsMediaChunk getLastMediaChunk() { From b9ab0cf137495eba377311c1b7dc572b78797a01 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 11 Jul 2019 17:38:31 +0100 Subject: [PATCH 193/807] Plumb DrmSessionManager into SsMediaSource PiperOrigin-RevId: 257624043 --- .../exoplayer2/demo/PlayerActivity.java | 4 ++- .../source/smoothstreaming/SsMediaPeriod.java | 23 +++++++++++++--- .../source/smoothstreaming/SsMediaSource.java | 26 +++++++++++++++++++ .../smoothstreaming/SsMediaPeriodTest.java | 2 ++ 4 files changed, 50 insertions(+), 5 deletions(-) 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 59d861e13d..3ade9173c6 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 @@ -480,7 +480,9 @@ public class PlayerActivity extends AppCompatActivity .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); case C.TYPE_OTHER: 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 6d4db3fbbb..286ec82ed6 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,6 +17,7 @@ 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; @@ -45,6 +46,7 @@ import java.util.List; private final SsChunkSource.Factory chunkSourceFactory; @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; @@ -62,6 +64,7 @@ import java.util.List; SsChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, @@ -70,11 +73,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); @@ -238,15 +242,26 @@ import java.util.List; this, allocator, positionUs, - DrmSessionManager.getDummyDrmSessionManager(), + 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 c053f255fc..3c0593200e 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(); @@ -117,6 +121,20 @@ public final class SsMediaSource 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 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,6 +338,7 @@ 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; @@ -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); @@ -483,6 +505,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs, /* tag= */ null); @@ -498,6 +521,7 @@ public final class SsMediaSource extends BaseMediaSource 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); @@ -553,6 +578,7 @@ public final class SsMediaSource extends BaseMediaSource chunkSourceFactory, mediaTransferListener, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, manifestLoaderErrorThrower, 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( From 31d20a9a973c3310990c87b60e392a538304fda0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 11 Jul 2019 18:11:08 +0100 Subject: [PATCH 194/807] Add missing file header PiperOrigin-RevId: 257630168 --- publish.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.gradle b/publish.gradle index f293673c49..8cfc2b2ea1 100644 --- a/publish.gradle +++ b/publish.gradle @@ -60,6 +60,7 @@ static void addLicense(File 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) From ccc82cdb4acf174a77b120a2dbf898b7865bcb3d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jul 2019 08:54:41 +0100 Subject: [PATCH 195/807] Remove some remaining references to manifests. PiperOrigin-RevId: 257757496 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 4 ++-- .../com/google/android/exoplayer2/source/MediaSource.java | 6 +++--- .../android/exoplayer2/source/dash/DashMediaSource.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) 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 68a386d2de..4e062dcb5e 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 @@ -563,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) 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 10e29f3f44..bd87eb6509 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 @@ -239,12 +239,12 @@ public interface MediaSource { } /** - * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest - * updates. + * Starts source preparation. * *

        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 SourceInfoRefreshListener#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. 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 92c47f2f12..f7387ee77c 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 @@ -366,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). + * SourceInfoRefreshListener#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; /** From df81bd6a682289ec7781cea2249353f6a71bceab Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jul 2019 08:55:08 +0100 Subject: [PATCH 196/807] Make ExtractorMediaSource a CompositeMediaSource instead of just wrapping. It's easy to forget to forward methods when using basic wrapping. For example, ExtractorMediaSource.addEventListener is currently a no-op because it's not forwarded. PiperOrigin-RevId: 257757556 --- .../source/ExtractorMediaSource.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) 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 2bcaad4fce..7332ed74e0 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 @@ -35,8 +35,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 @@ -340,12 +339,14 @@ public final class ExtractorMediaSource extends BaseMediaSource @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - progressiveMediaSource.prepareSource(/* listener= */ this, 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,16 +359,6 @@ public final class ExtractorMediaSource extends BaseMediaSource progressiveMediaSource.releasePeriod(mediaPeriod); } - @Override - protected void releaseSourceInternal() { - progressiveMediaSource.releaseSource(/* listener= */ this); - } - - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - refreshSourceInfo(timeline); - } - @Deprecated private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { From 510f1883b055b042425555378cdf7302decf88b1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 12 Jul 2019 12:01:18 +0100 Subject: [PATCH 197/807] Plumb DrmSessionManager into ProgressiveMediaSource PiperOrigin-RevId: 257777513 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../source/ExtractorMediaSource.java | 2 + .../source/ProgressiveMediaPeriod.java | 42 +++++++++++++------ .../source/ProgressiveMediaSource.java | 23 ++++++++++ 4 files changed, 58 insertions(+), 13 deletions(-) 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 3ade9173c6..249223bff5 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 @@ -486,7 +486,9 @@ public class PlayerActivity extends AppCompatActivity case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory).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); } 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 7332ed74e0..9e7da87766 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; @@ -325,6 +326,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { uri, dataSourceFactory, extractorsFactory, + DrmSessionManager.getDummyDrmSessionManager(), loadableLoadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, 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 a56d14083e..83145d04b0 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; @@ -195,6 +201,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sampleQueue.discardToEnd(); } } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); callback = null; @@ -432,29 +441,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( - formatHolder, - buffer, - formatRequired, - /* allowOnlyClearBuffers= */ false, - loadingFinished, - lastSeekPositionUs); + sampleQueueReaders[sampleQueueIndex].read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { - maybeStartDeferredRetry(track); + maybeStartDeferredRetry(sampleQueueIndex); } return result; } @@ -667,6 +679,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; } @@ -868,7 +886,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void maybeThrowError() throws IOException { - ProgressiveMediaPeriod.this.maybeThrowError(); + ProgressiveMediaPeriod.this.maybeThrowError(track); } @Override 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 42ec237b3e..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; @@ -248,6 +270,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSource, extractorsFactory.createExtractors(), + drmSessionManager, loadableLoadErrorHandlingPolicy, createEventDispatcher(id), this, From 3fe0b1a6fee8e7631caa5a6f84306396ee6999ad Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jul 2019 18:32:58 +0100 Subject: [PATCH 198/807] Rename SourceInfoRefreshListener to MediaSourceCaller. This better reflects its usage as a caller identifier and not just a listener. PiperOrigin-RevId: 257827188 --- RELEASENOTES.md | 7 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 5 ++- .../exoplayer2/offline/DownloadHelper.java | 5 ++- .../exoplayer2/source/BaseMediaSource.java | 21 +++++----- .../source/CompositeMediaSource.java | 25 ++++------- .../exoplayer2/source/MediaPeriod.java | 4 +- .../exoplayer2/source/MediaSource.java | 42 ++++++++----------- .../source/ConcatenatingMediaSourceTest.java | 8 ++-- .../source/dash/DashMediaSource.java | 4 +- .../testutil/MediaSourceTestRunner.java | 11 ++--- 10 files changed, 59 insertions(+), 73 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a5e635668..12babc1688 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,9 +15,10 @@ * Add VR player demo. * Wrap decoder exceptions in a new `DecoderException` class and report as renderer error. -* Do not pass the manifest to callbacks of Player.EventListener and - SourceInfoRefreshListener anymore. Instead make it accessible through - Player.getCurrentManifest() and Timeline.Window.manifest. +* Do not pass the manifest to callbacks of `Player.EventListener` and + `SourceInfoRefreshListener` anymore. Instead make it accessible through + `Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename + `SourceInfoRefreshListener` to `MediaSourceCaller`. * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). 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 5f53427fca..e313c9aedf 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,7 +265,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { 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 17bc304db3..139c6ad794 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 @@ -31,6 +31,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; @@ -800,7 +801,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; @@ -892,7 +893,7 @@ public final class DownloadHelper { } } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { 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 124f70c64c..886952f5c3 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 @@ -32,14 +32,14 @@ import java.util.ArrayList; */ public abstract class BaseMediaSource implements MediaSource { - private final ArrayList sourceInfoListeners; + private final ArrayList mediaSourceCallers; private final MediaSourceEventListener.EventDispatcher eventDispatcher; @Nullable private Looper looper; @Nullable private Timeline timeline; public BaseMediaSource() { - sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); + mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); eventDispatcher = new MediaSourceEventListener.EventDispatcher(); } @@ -67,8 +67,8 @@ public abstract class BaseMediaSource implements MediaSource { */ protected final void refreshSourceInfo(Timeline timeline) { this.timeline = timeline; - for (SourceInfoRefreshListener listener : sourceInfoListeners) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline); + for (MediaSourceCaller caller : mediaSourceCallers) { + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -127,23 +127,22 @@ 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); + mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline); + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @Override - public final void releaseSource(SourceInfoRefreshListener listener) { - sourceInfoListeners.remove(listener); - if (sourceInfoListeners.isEmpty()) { + public final void releaseSource(MediaSourceCaller caller) { + mediaSourceCallers.remove(caller); + if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; releaseSourceInternal(); 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 612ad33f9d..3eac3df5fe 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 @@ -61,7 +61,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @CallSuper protected void releaseSourceInternal() { for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.listener); + childSource.mediaSource.releaseSource(childSource.caller); childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); @@ -91,17 +91,12 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - SourceInfoRefreshListener sourceListener = - new SourceInfoRefreshListener() { - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - onChildSourceInfoRefreshed(id, source, timeline); - } - }; + 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); } /** @@ -111,7 +106,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); } @@ -157,15 +152,13 @@ public abstract class CompositeMediaSource extends BaseMediaSource { 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; } } 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 f86be8afc2..f076eae32c 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,8 +58,7 @@ 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)} will be + * 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 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 bd87eb6509..c3219a03c1 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 @@ -30,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. @@ -45,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 the timeline has been refreshed. + * 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. */ - default void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { - // Do nothing. - } + 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. */ @@ -239,24 +235,23 @@ public interface MediaSource { } /** - * Starts source preparation. + * Registers a {@link MediaSourceCaller} and starts source preparation if needed. * *

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

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

          {@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. @@ -288,12 +283,11 @@ public interface MediaSource { void releasePeriod(MediaPeriod mediaPeriod); /** - * Removes a listener for timeline and/or manifest updates and releases the source if no longer - * required. + * Unregisters a caller and releases the source if no longer required. * *

          Should not be called directly from application code. * - * @param listener The listener to be removed. + * @param caller The {@link MediaSourceCaller} to be unregistered. */ - void releaseSource(SourceInfoRefreshListener listener); + void releaseSource(MediaSourceCaller caller); } 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 c587d85a85..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; @@ -628,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 { 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 f7387ee77c..576491b464 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 @@ -366,8 +366,8 @@ public final class DashMediaSource extends BaseMediaSource { /** * The interval in milliseconds between invocations of {@link - * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline)} 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; /** diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 6d626088fc..211e85d30c 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/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,10 +337,9 @@ 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) { From bbcd1126b214596006b35fe104d7a3a78ba2ae5a Mon Sep 17 00:00:00 2001 From: tonihei Date: Sun, 14 Jul 2019 15:36:00 +0100 Subject: [PATCH 199/807] Remove DownloadService from nullness blacklist. PiperOrigin-RevId: 258038961 --- .../exoplayer2/offline/DownloadService.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) 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..84b6f5870d 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 @@ -175,7 +175,7 @@ public abstract class DownloadService extends Service { @Nullable private final String channelId; @StringRes private final int channelNameResourceId; - private DownloadManager downloadManager; + @Nullable private DownloadManager downloadManager; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; @@ -575,6 +575,7 @@ 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: @@ -640,8 +641,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 +777,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 +785,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 +794,7 @@ public abstract class DownloadService extends Service { public void stopPeriodicUpdates() { periodicUpdatesStarted = false; - handler.removeCallbacks(updateRunnable); + handler.removeCallbacksAndMessages(null); } public void showNotificationIfNotAlready() { @@ -810,12 +810,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 +840,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 +895,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 { From af98883a7bffc6b8a760b84737012ddd5cfcd622 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Mon, 15 Jul 2019 11:35:32 +0200 Subject: [PATCH 200/807] Reintroducing existing logic as requested here https://github.com/google/ExoPlayer/pull/6178#pullrequestreview-261298162 --- .../trackselection/DefaultTrackSelector.java | 131 ++++++++++-------- 1 file changed, 75 insertions(+), 56 deletions(-) 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 511a974a0e..c0de66516b 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 @@ -1555,29 +1555,39 @@ public class DefaultTrackSelector extends MappingTrackSelector { TextTrackScore selectedTextTrackScore = null; int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { - // The below behaviour is different from video and audio track selection - // i.e. do not perform a text track pre selection if there are no preferredTextLanguage requested. - if (C.TRACK_TYPE_TEXT == mappedTrackInfo.getRendererType(i) && params.preferredTextLanguage != null) { - Pair textSelection = - selectTextTrack( - mappedTrackInfo.getTrackGroups(i), - rendererFormatSupports[i], - params); - 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. - definitions[selectedTextRendererIndex] = null; + int trackType = mappedTrackInfo.getRendererType(i); + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + case C.TRACK_TYPE_AUDIO: + // Already done. Do nothing. + break; + case C.TRACK_TYPE_TEXT: + Pair textSelection = + selectTextTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + params, + selectedAudioLanguage); + 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. + definitions[selectedTextRendererIndex] = null; + } + definitions[i] = textSelection.first; + selectedTextTrackScore = textSelection.second; + selectedTextRendererIndex = i; } - TrackSelection.Definition definition = textSelection.first; - definitions[i] = definition; - selectedTextTrackScore = textSelection.second; - selectedTextRendererIndex = i; + break; + default: + definitions[i] = + selectOtherTrack( + trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); + break; } } - } return definitions; } @@ -2052,7 +2062,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected Pair selectTextTrack( TrackGroupArray groups, int[][] formatSupport, - Parameters params) + Parameters params, + @Nullable String selectedAudioLanguage) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = C.INDEX_UNSET; @@ -2064,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex]); + TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); if ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; @@ -2497,29 +2508,49 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Represents how well an text track matches the selection {@link Parameters}. */ protected static final class TextTrackScore implements Comparable { - private final boolean isWithinRendererCapabilities; - private final int preferredLanguageScore; - private final int localeLanguageMatchIndex; - private final int localeLanguageScore; - private final boolean isDefaultSelectionFlag; + private final boolean isDefault; + private final boolean isForced; + private final int languageScore; + private final boolean trackHasNoLanguage; + private int bestMatchScore = 0; - public TextTrackScore(Format format, Parameters parameters, int formatSupport) { - isWithinRendererCapabilities = isSupported(formatSupport, false); - preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); - isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - String[] localeLanguages = Util.getSystemLanguageCodes(); - int bestMatchIndex = Integer.MAX_VALUE; - int bestMatchScore = 0; - for (int i = 0; i < localeLanguages.length; i++) { - int score = getFormatLanguageScore(format, localeLanguages[i]); - if (score > 0) { - bestMatchIndex = i; - bestMatchScore = score; - break; + public TextTrackScore( + Format format, + Parameters parameters, + int trackFormatSupport, + @Nullable String selectedAudioLanguage) { + languageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + int maskedSelectionFlags = + format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; + isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + trackHasNoLanguage = formatHasNoLanguage(format); + + if (languageScore > 0 || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage)) { + if (isDefault) { + bestMatchScore = 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. + bestMatchScore = 7; + } else { + bestMatchScore = 3; } + bestMatchScore += languageScore; + } else if (isDefault) { + bestMatchScore = 2; + } else if (isForced + && (languageScore > 0 + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { + bestMatchScore = 1; + } else { + // Track should not be selected. + bestMatchScore = -1; + } + if (isSupported(trackFormatSupport, false)) { + bestMatchScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } - localeLanguageMatchIndex = bestMatchIndex; - localeLanguageScore = bestMatchScore; } /** @@ -2531,20 +2562,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Override public int compareTo(@NonNull 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.isDefaultSelectionFlag != other.isDefaultSelectionFlag) { - return this.isDefaultSelectionFlag ? 1 : -1; - } - if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) { - return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex); - } - if (this.localeLanguageScore != other.localeLanguageScore) { - return compareInts(this.localeLanguageScore, other.localeLanguageScore); + if (this.bestMatchScore != other.bestMatchScore) { + return compareInts(this.bestMatchScore, other.bestMatchScore); } return 0; } From 1909987dc8d63ef6504d7c872e3ce28b9cc560c7 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Mon, 15 Jul 2019 16:05:01 +0200 Subject: [PATCH 201/807] Change all the conditions and scores that are currently in the constructor of TextTrackScore to comparisons in TextTrackScore.compareTo --- .../trackselection/DefaultTrackSelector.java | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) 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 c0de66516b..b5d282f8a7 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 @@ -2508,49 +2508,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Represents how well an text track matches the selection {@link Parameters}. */ protected static final class TextTrackScore implements Comparable { + private final boolean isWithinRendererCapabilities; private final boolean isDefault; private final boolean isForced; - private final int languageScore; + private final int preferredLanguageScore; + private final int selectedAudioLanguageScore; private final boolean trackHasNoLanguage; - private int bestMatchScore = 0; + private final boolean selectUndeterminedTextLanguage; + private final boolean stringDefinesNoLang; public TextTrackScore( Format format, Parameters parameters, int trackFormatSupport, @Nullable String selectedAudioLanguage) { - languageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + isWithinRendererCapabilities = isSupported(trackFormatSupport, false); int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); + selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage); trackHasNoLanguage = formatHasNoLanguage(format); - - if (languageScore > 0 || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage)) { - if (isDefault) { - bestMatchScore = 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. - bestMatchScore = 7; - } else { - bestMatchScore = 3; - } - bestMatchScore += languageScore; - } else if (isDefault) { - bestMatchScore = 2; - } else if (isForced - && (languageScore > 0 - || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { - bestMatchScore = 1; - } else { - // Track should not be selected. - bestMatchScore = -1; - } - if (isSupported(trackFormatSupport, false)) { - bestMatchScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } + selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage; + stringDefinesNoLang = stringDefinesNoLanguage(selectedAudioLanguage); } /** @@ -2562,10 +2543,38 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Override public int compareTo(@NonNull TextTrackScore other) { - if (this.bestMatchScore != other.bestMatchScore) { - return compareInts(this.bestMatchScore, other.bestMatchScore); + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if ((this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) == + (other.preferredLanguageScore > 0 || (other.selectUndeterminedTextLanguage && other.trackHasNoLanguage))) { + if (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage + && this.trackHasNoLanguage)) { + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.isForced != other.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. + return !this.isForced ? 1 : -1; + } + return (this.preferredLanguageScore > other.preferredLanguageScore) ? 1 : -1; + } else { + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if ((this.isForced && (this.selectedAudioLanguageScore > 0 || (this.trackHasNoLanguage && this.stringDefinesNoLang))) != + (other.isForced && (other.selectedAudioLanguageScore > 0 || (other.trackHasNoLanguage && other.stringDefinesNoLang)))) { + return (this.isForced && (this.selectedAudioLanguageScore > 0 + || (this.trackHasNoLanguage && this.stringDefinesNoLang))) ? 1 : -1; + } + // Track should not be selected. + return -1; + } + } else { + return (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) ? 1 : -1; } - return 0; } } } From 1d4d10517448e5428e7c2db969d1a8c330938be6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 Jul 2019 09:04:20 +0100 Subject: [PATCH 202/807] Compile with SDK 29 (Android Q) PiperOrigin-RevId: 258110603 --- RELEASENOTES.md | 1 + constants.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 12babc1688..46515d8fa8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,7 @@ `SourceInfoRefreshListener` to `MediaSourceCaller`. * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Set `compileSdkVersion` to 29 to use Android Q APIs. ### 2.10.3 ### diff --git a/constants.gradle b/constants.gradle index e857d5a812..c8136ea471 100644 --- a/constants.gradle +++ b/constants.gradle @@ -17,7 +17,7 @@ project.ext { releaseVersionCode = 2010003 minSdkVersion = 16 targetSdkVersion = 28 - compileSdkVersion = 28 + compileSdkVersion = 29 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' robolectricVersion = '4.3' From 2b5c42e02770475c72e45c887f9f2db8a688db4f Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 15 Jul 2019 15:48:09 +0100 Subject: [PATCH 203/807] Fix some inline parameter name comments. The name of this parameter recently changed in https://github.com/google/ExoPlayer/commit/3fe0b1a6fee8e7631caa5a6f84306396ee6999ad and I forgot to change these inline comment usages. PiperOrigin-RevId: 258160659 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++-- .../com/google/android/exoplayer2/offline/DownloadHelper.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 e313c9aedf..0fc7242279 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 @@ -442,7 +442,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); } @@ -914,7 +914,7 @@ import java.util.concurrent.atomic.AtomicBoolean; startPositionUs); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(/* listener= */ this); + mediaSource.releaseSource(/* caller= */ this); mediaSource = null; } } 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 139c6ad794..4b5bf3c8a4 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 @@ -852,7 +852,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: From 7760eca238d68c2729bb95747bf01e64cbb2fd7b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 16 Jul 2019 03:30:40 +0100 Subject: [PATCH 204/807] Deep compare formats in SampleMetadataQueue instead of shallow compare PiperOrigin-RevId: 258285645 --- .../android/exoplayer2/source/SampleMetadataQueue.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 542565e70d..78b3a35549 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 @@ -227,7 +227,7 @@ import com.google.android.exoplayer2.util.Util; return SampleQueue.PEEK_RESULT_NOTHING; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex] != downstreamFormat) { + if (formats[relativeReadIndex].equals(downstreamFormat)) { return SampleQueue.PEEK_RESULT_FORMAT; } else { return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 @@ -275,7 +275,7 @@ import com.google.android.exoplayer2.util.Util; buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { + && (formatRequired || !upstreamFormat.equals(downstreamFormat))) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; } else { @@ -284,7 +284,7 @@ import com.google.android.exoplayer2.util.Util; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + if (formatRequired || !formats[relativeReadIndex].equals(downstreamFormat)) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } @@ -422,7 +422,6 @@ 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. return false; } else { upstreamFormat = format; From 09147ff5481e9741a29534a795d99a3bda486a72 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 16 Jul 2019 13:03:10 +0100 Subject: [PATCH 205/807] Add missing consts for consistency These getters do not modify the instance. PiperOrigin-RevId: 258345084 --- extensions/flac/src/main/jni/include/flac_parser.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index d9043e9548..f09a22e951 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -48,9 +48,11 @@ class FLACParser { return mStreamInfo; } - bool isVorbisCommentsValid() { return mVorbisCommentsValid; } + bool isVorbisCommentsValid() const { return mVorbisCommentsValid; } - std::vector getVorbisComments() { return mVorbisComments; } + const std::vector& getVorbisComments() const { + return mVorbisComments; + } int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); From 01376443b36f70c1fe377e4898959dfa34f6024a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 16 Jul 2019 16:39:23 +0100 Subject: [PATCH 206/807] Add MediaSource.enable/disable. These methods helps to indicate that a media source isn't used to create new periods in the immediate term and thus limited resources can be released. PiperOrigin-RevId: 258373069 --- RELEASENOTES.md | 2 + .../exoplayer2/source/BaseMediaSource.java | 46 ++++++++++++++++-- .../source/CompositeMediaSource.java | 41 +++++++++++++++- .../source/ConcatenatingMediaSource.java | 48 ++++++++++++++++++- .../exoplayer2/source/MediaSource.java | 48 +++++++++++++++---- 5 files changed, 171 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 46515d8fa8..9d66a6f800 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Set `compileSdkVersion` to 29 to use Android Q APIs. +* Add `enable` and `disable` methods to `MediaSource` to improve resource + management in playlists. ### 2.10.3 ### 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 886952f5c3..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,6 +22,7 @@ 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 @@ -33,6 +34,7 @@ import java.util.ArrayList; public abstract class BaseMediaSource implements MediaSource { private final ArrayList mediaSourceCallers; + private final HashSet enabledMediaSourceCallers; private final MediaSourceEventListener.EventDispatcher eventDispatcher; @Nullable private Looper looper; @@ -40,11 +42,13 @@ public abstract class BaseMediaSource implements MediaSource { public BaseMediaSource() { 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 @@ -54,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(); @@ -115,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,22 +145,47 @@ public abstract class BaseMediaSource implements MediaSource { MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); + Timeline timeline = this.timeline; mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; + enabledMediaSourceCallers.add(caller); prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { + enable(caller); caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } + @Override + 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; + enabledMediaSourceCallers.clear(); releaseSourceInternal(); + } else { + disable(caller); } } } 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 3eac3df5fe..3672c304cc 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 @@ -37,7 +37,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @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<>(); } @@ -57,6 +57,22 @@ public abstract class CompositeMediaSource extends BaseMediaSource { } } + @Override + @CallSuper + protected void enableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + 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() { @@ -97,6 +113,29 @@ public abstract class CompositeMediaSource extends BaseMediaSource { childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); 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); } /** 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 18d5c49fb4..669a0e7bb4 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 @@ -35,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; @@ -68,6 +69,7 @@ public final 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; @@ -131,6 +133,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(); this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); this.pendingOnCompletionActions = new HashSet<>(); + this.enabledMediaSourceHolders = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; addMediaSources(Arrays.asList(mediaSources)); @@ -418,7 +421,8 @@ public final 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); 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 c3219a03c1..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 @@ -235,7 +235,8 @@ public interface MediaSource { } /** - * Registers a {@link MediaSourceCaller} and starts source preparation if needed. + * 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. * @@ -255,17 +256,31 @@ public interface MediaSource { /** * 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. @@ -275,18 +290,35 @@ 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); /** - * Unregisters a caller 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. * + *

          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 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); From 5e4f52541d668f5a8950a80b28f6a8bbcaca83b1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 10:10:39 +0100 Subject: [PATCH 207/807] Extend RK video_decoder workaround to newer API levels Issue: #6184 PiperOrigin-RevId: 258527533 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 30083cb849..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 @@ -1856,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); } From a3ded59f28e05495446a739aa2f014013859be58 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 13:59:14 +0100 Subject: [PATCH 208/807] Check codec profile/level for AV1 Add appropriate unit tests. PiperOrigin-RevId: 258552404 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 77 ++++++++++++++++++- .../mediacodec/MediaCodecUtilTest.java | 25 ++++++ 2 files changed, 99 insertions(+), 3 deletions(-) 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..455ee6c034 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 @@ -81,10 +81,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; @@ -239,8 +242,6 @@ public final class MediaCodecUtil { if (codec == 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("\\."); switch (parts[0]) { case CODEC_ID_AVC1: @@ -254,6 +255,8 @@ public final class MediaCodecUtil { case CODEC_ID_DVHE: case CODEC_ID_DVH1: return getDolbyVisionProfileAndLevel(codec, parts); + case CODEC_ID_AV01: + return getAv1ProfileAndLevel(codec, parts); case CODEC_ID_MP4A: return getAacCodecProfileAndLevel(codec, parts); default: @@ -684,6 +687,48 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + private static Pair getAv1ProfileAndLevel(String codec, String[] parts) { + 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; + } + + // TODO: Recognize HDR profiles. Currently, the profile is assumed to be either Main8 or Main10. + // See [Internal: b/124435216]. + 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 { + 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 +1055,32 @@ public final class MediaCodecUtil { DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + 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/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java index a84c6f5d7b..05d92e0783 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 @@ -60,6 +60,31 @@ 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_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(); From 1a479387f2a4939d298ee8c27d8c98862bd54842 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jul 2019 16:32:11 +0100 Subject: [PATCH 209/807] Fix the equals check PiperOrigin-RevId: 258574110 --- .../google/android/exoplayer2/source/SampleMetadataQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 78b3a35549..89160f45f3 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 @@ -227,7 +227,7 @@ import com.google.android.exoplayer2.util.Util; return SampleQueue.PEEK_RESULT_NOTHING; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex].equals(downstreamFormat)) { + if (!formats[relativeReadIndex].equals(downstreamFormat)) { return SampleQueue.PEEK_RESULT_FORMAT; } else { return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 From 049f3cf5cda00fb5acdd9151f6d9040a15e843d9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 17 Jul 2019 17:33:36 +0100 Subject: [PATCH 210/807] Keep default start position (TIME_UNSET) as content position for preroll ads. If we use the default start position, we currently resolve it immediately even if we need to play an ad first, and later try to project forward again if we believe that the default start position should be used. This causes problems if a specific start position is set and the later projection after the preroll ad shouldn't take place. The problem is solved by keeping the content position as TIME_UNSET (= default position) if an ad needs to be played first. The content after the ad can then be resolved to its current default position if needed. PiperOrigin-RevId: 258583948 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 7 ++- .../android/exoplayer2/MediaPeriodInfo.java | 3 +- .../android/exoplayer2/MediaPeriodQueue.java | 21 ++++---- .../android/exoplayer2/PlaybackInfo.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 51 +++++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 2 +- 8 files changed, 77 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d66a6f800..3382f01e8a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,7 @@ * Set `compileSdkVersion` to 29 to use Android Q APIs. * Add `enable` and `disable` methods to `MediaSource` to improve resource management in playlists. +* Fix issue where initial seek positions get ignored when playing a preroll ad. ### 2.10.3 ### 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 73107aa98e..f380af968c 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 @@ -493,7 +493,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(); } 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 0fc7242279..38cdb57fc8 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 @@ -1303,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. 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 e47d8af381..0dacd4df30 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 @@ -144,7 +144,9 @@ import com.google.android.exoplayer2.util.Assertions; MediaPeriodInfo info) { long rendererPositionOffsetUs = loading == null - ? (info.id.isAd() ? info.contentPositionUs : 0) + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( @@ -560,6 +562,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 +571,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 +591,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 +621,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 1eedae08b6..669f41ca13 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 @@ -45,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. */ 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 61b8418411..39046b52ce 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; @@ -2590,6 +2591,56 @@ 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); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { 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 f7c6694409..b61c5f9b2c 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 @@ -416,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); } From bee35ed9d7aff3d7957d68b291b307054f3e5704 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 17 Jul 2019 18:07:28 +0100 Subject: [PATCH 211/807] Fix DeprecationMismatch errors PiperOrigin-RevId: 258590215 --- .../java/com/google/android/exoplayer2/Format.java | 12 ++++++++++++ .../exoplayer2/source/ExtractorMediaSource.java | 5 ++++- .../android/exoplayer2/upstream/HttpDataSource.java | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) 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 f06c9da048..df01df1708 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 @@ -179,6 +179,10 @@ public final class Format implements Parcelable { // 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, @@ -358,6 +362,10 @@ public final class Format implements Parcelable { // 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, @@ -763,6 +771,10 @@ public final class Format implements Parcelable { // Generic. + /** + * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int, + * int, String)} instead. + */ @Deprecated public static Format createContainerFormat( @Nullable String id, 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 9e7da87766..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 @@ -58,7 +58,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { } - /** Use {@link ProgressiveMediaSource.Factory} instead. */ + /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */ @Deprecated public static final class Factory implements MediaSourceFactory { @@ -221,6 +221,9 @@ public final class ExtractorMediaSource extends CompositeMediaSource { } } + /** + * @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; 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() { From 80d5dabd525727f55a4ad5b74ba67d58b1a9fd32 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jul 2019 18:22:04 +0100 Subject: [PATCH 212/807] Fix DataSchemeDataSource re-opening and range requests Issue:#6192 PiperOrigin-RevId: 258592902 --- RELEASENOTES.md | 2 + .../upstream/DataSchemeDataSource.java | 24 ++++---- .../upstream/DataSchemeDataSourceTest.java | 60 +++++++++++++++++-- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3382f01e8a..a769ff5f0c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ * Add `enable` and `disable` methods to `MediaSource` to improve resource management in playlists. * Fix issue where initial seek positions get ignored when playing a preroll ad. +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). ### 2.10.3 ### 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 03804fa577..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 @@ -33,8 +33,8 @@ public final class DataSchemeDataSource extends BaseDataSource { @Nullable private DataSpec dataSpec; @Nullable private byte[] data; - private int dataLength; - private int bytesRead; + private int endPosition; + private int readPosition; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -44,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)) { @@ -57,17 +58,21 @@ public final class DataSchemeDataSource extends BaseDataSource { if (uriParts[0].contains(";base64")) { try { data = Base64.decode(dataString, 0); - dataLength = data.length; } catch (IllegalArgumentException e) { throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); } } else { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); - dataLength = data.length; + } + 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 dataLength; + return (long) endPosition - readPosition; } @Override @@ -75,13 +80,13 @@ public final class DataSchemeDataSource extends BaseDataSource { if (readLength == 0) { return 0; } - int remainingBytes = dataLength - bytesRead; + int remainingBytes = endPosition - readPosition; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(castNonNull(data), bytesRead, buffer, offset, readLength); - bytesRead += readLength; + System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength); + readPosition += readLength; bytesTransferred(readLength); return readLength; } @@ -93,12 +98,11 @@ public final class DataSchemeDataSource extends BaseDataSource { } @Override - public void close() throws IOException { + public void close() { if (data != null) { data = null; transferEnded(); } dataSpec = null; } - } 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); } } From c779e84cbb911b33357a909624804a5870b99340 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 10:08:19 +0100 Subject: [PATCH 213/807] Switch language normalization to 2-letter language codes. 2-letter codes (ISO 639-1) are the standard Android normalization and thus we should prefer them to 3-letter codes (although both are technically allowed according the BCP47). This helps in two ways: 1. It simplifies app interaction with our normalized language codes as the Locale class makes it easy to convert a 2-letter to a 3-letter code but not the other way round. 2. It better normalizes codes on API<21 where we previously had issues with language+country codes (see tests). 3. It allows us to normalize both ISO 639-2/T and ISO 639-2/B codes to the same language. PiperOrigin-RevId: 258729728 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 10 +-- .../google/android/exoplayer2/util/Util.java | 80 ++++++++++++++++--- .../android/exoplayer2/util/UtilTest.java | 46 ++++++++--- .../playlist/HlsMasterPlaylistParserTest.java | 2 +- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a769ff5f0c..7194882758 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ * Fix issue where initial seek positions get ignored when playing a preroll ad. * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. ### 2.10.3 ### 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 949bd178ea..b8dd40f8bd 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 @@ -2318,14 +2318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(format.language, language)) { return 3; } - // Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk") + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") if (format.language.startsWith(language) || language.startsWith(format.language)) { 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))) { + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") + String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0]; + String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; + if (formatMainLanguage.equals(queryMainLanguage)) { return 1; } return 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 32e9c32a53..a8aa2c630b 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() {} /** @@ -465,18 +470,25 @@ 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) { + Locale locale = getLocaleForLanguageTag(language); + String localeLanguage = locale.getLanguage(); + int localeLanguageLength = localeLanguage.length(); + if (localeLanguageLength == 0) { + // Return original language for invalid language tags. return toLowerInvariant(language); + } else if (localeLanguageLength == 3) { + // Locale.toLanguageTag will ensure a normalized well-formed output. However, 3-letter + // ISO 639-2 language codes will not be converted to 2-letter ISO 639-1 codes automatically. + if (languageTagIso3ToIso2 == null) { + languageTagIso3ToIso2 = createIso3ToIso2Map(); + } + String iso2Language = languageTagIso3ToIso2.get(localeLanguage); + if (iso2Language != null) { + localeLanguage = iso2Language; + } } + String normTag = getLocaleLanguageTag(locale); + return toLowerInvariant(localeLanguage + normTag.substring(localeLanguageLength)); } /** @@ -2028,6 +2040,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/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 9abec0cd8f..f85ee37c07 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,15 @@ 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("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-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 +284,38 @@ 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("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); 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/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 From e25340be3d424f8cdba2531984e2db6624382392 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jul 2019 12:40:40 +0100 Subject: [PATCH 214/807] Pass format instead of codec string when getting profile and level AV1 profile recognition requires additional info contained in format. PiperOrigin-RevId: 258746315 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 29 ++++++------ .../exoplayer2/mediacodec/MediaCodecUtil.java | 29 ++++++------ .../video/MediaCodecVideoRenderer.java | 6 +-- .../mediacodec/MediaCodecUtilTest.java | 44 +++++++++++++++++-- 4 files changed, 71 insertions(+), 37 deletions(-) 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 3310b0dc8b..acaf798b41 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 @@ -198,7 +198,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 +226,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 +261,7 @@ public final class MediaCodecInfo { return true; } } - logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); return false; } @@ -279,8 +279,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 +313,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; } 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 455ee6c034..df5ca05972 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 @@ -230,35 +230,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; } - 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(codec, parts); + return getAv1ProfileAndLevel(format.codecs, parts); case CODEC_ID_MP4A: - return getAacCodecProfileAndLevel(codec, parts); + return getAacCodecProfileAndLevel(format.codecs, parts); default: return null; } 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 d9d81cf6d4..6e3114d1b1 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 @@ -390,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) { @@ -1194,8 +1193,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); 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 05d92e0783..c485ff49f6 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,8 @@ 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.Format; +import com.google.android.exoplayer2.util.MimeTypes; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,17 +89,53 @@ public final class MediaCodecUtilTest { @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); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); assertThat(codecProfileAndLevel).isNotNull(); assertThat(codecProfileAndLevel.first).isEqualTo(profile); assertThat(codecProfileAndLevel.second).isEqualTo(level); From c67f18764f038f8a39a1c6aaacea5458f136427d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jul 2019 13:45:00 +0100 Subject: [PATCH 215/807] Move Format equality check back to write side of sample queue PiperOrigin-RevId: 258752996 --- .../source/SampleMetadataQueue.java | 19 +++++++++--- .../exoplayer2/source/SampleQueueTest.java | 31 +++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) 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 89160f45f3..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 @@ -62,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() { @@ -96,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; @@ -227,7 +229,7 @@ import com.google.android.exoplayer2.util.Util; return SampleQueue.PEEK_RESULT_NOTHING; } int relativeReadIndex = getRelativeIndex(readPosition); - if (!formats[relativeReadIndex].equals(downstreamFormat)) { + if (formats[relativeReadIndex] != downstreamFormat) { return SampleQueue.PEEK_RESULT_FORMAT; } else { return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 @@ -274,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.equals(downstreamFormat))) { + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; } else { @@ -284,7 +285,7 @@ import com.google.android.exoplayer2.util.Util; } int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || !formats[relativeReadIndex].equals(downstreamFormat)) { + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } @@ -422,7 +423,16 @@ import com.google.android.exoplayer2.util.Util; } upstreamFormatRequired = false; if (Util.areEqual(format, upstreamFormat)) { + // 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; @@ -450,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/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index bfc6bb52c9..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 @@ -129,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). @@ -140,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); From e4f849076c01b2327a9083bfa5d62f0d877149e3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 18 Jul 2019 14:00:18 +0100 Subject: [PATCH 216/807] Remove unused extractor constructors PiperOrigin-RevId: 258754710 --- .../extractor/DefaultExtractorsFactory.java | 3 +-- .../exoplayer2/extractor/ts/Ac3Extractor.java | 9 ++------- .../exoplayer2/extractor/ts/Ac4Extractor.java | 9 +-------- .../exoplayer2/extractor/ts/AdtsExtractor.java | 18 ++++++------------ .../extractor/ts/AdtsExtractorSeekTest.java | 4 +--- .../extractor/ts/AdtsExtractorTest.java | 5 +---- 6 files changed, 12 insertions(+), 36 deletions(-) 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/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 0a0755327c..2f46744ea0 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 @@ -46,18 +46,13 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SYNC_FRAME_SIZE = 2786; private static final int ID3_TAG = 0x00494433; - 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); } @@ -141,7 +136,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 4db02e0d83..2e8dcd952b 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 @@ -54,7 +54,6 @@ public final class Ac4Extractor implements Extractor { private static final int ID3_TAG = 0x00494433; - private final long firstSampleTimestampUs; private final Ac4Reader reader; private final ParsableByteArray sampleData; @@ -62,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); } @@ -152,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 d1e3217e30..3dd88fbb51 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 @@ -83,7 +83,6 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray packetBuffer; private final ParsableByteArray scratch; private final ParsableBitArray scratchBits; - private final long firstStreamSampleTimestampUs; @Nullable private ExtractorOutput extractorOutput; @@ -94,22 +93,17 @@ 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); @@ -172,7 +166,7 @@ public final class AdtsExtractor implements Extractor { public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); - firstSampleTimestampUs = firstStreamSampleTimestampUs + timeUs; + firstSampleTimestampUs = timeUs; } @Override 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"); } } From aeb2fefe483323ed07ab1e0881ae351fedda13c9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 16:18:49 +0100 Subject: [PATCH 217/807] Further language normalization tweaks for API < 21. 1. Using the Locale on API<21 doesn't make any sense because it's a no-op anyway. Slightly restructured the code to avoid that. 2. API<21 often reports languages with non-standard underscores instead of dashes. Normalize that too. 3. Some invalid language tags on API>21 get normalized to "und". Use original tag in such a case. Issue:#6153 PiperOrigin-RevId: 258773463 --- RELEASENOTES.md | 3 + .../google/android/exoplayer2/util/Util.java | 57 +++++++++---------- .../android/exoplayer2/util/UtilTest.java | 15 +++++ 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7194882758..30098e01df 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,9 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). ### 2.10.3 ### 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 a8aa2c630b..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 @@ -470,25 +470,31 @@ public final class Util { if (language == null) { return null; } - Locale locale = getLocaleForLanguageTag(language); - String localeLanguage = locale.getLanguage(); - int localeLanguageLength = localeLanguage.length(); - if (localeLanguageLength == 0) { - // Return original language for invalid language tags. - return toLowerInvariant(language); - } else if (localeLanguageLength == 3) { - // Locale.toLanguageTag will ensure a normalized well-formed output. However, 3-letter - // ISO 639-2 language codes will not be converted to 2-letter ISO 639-1 codes automatically. + // 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(localeLanguage); + String iso2Language = languageTagIso3ToIso2.get(mainLanguage); if (iso2Language != null) { - localeLanguage = iso2Language; + normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); } } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(localeLanguage + normTag.substring(localeLanguageLength)); + return normalizedTag; } /** @@ -1982,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) { 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 f85ee37c07..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,10 +268,14 @@ public class UtilTest { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { + 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"); @@ -284,9 +288,20 @@ public class UtilTest { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { + 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"); } From 08624113d409a448b4aa227c9532c4eedeeb2266 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jul 2019 17:40:57 +0100 Subject: [PATCH 218/807] Correctly mask playback info changes in ExoPlayerImpl. PlaybackInfo changes are one of the last ones not masked and reported in the same way as all other changes. The main change to support this is to also mask the parameters set in DefaultAudioSink. PiperOrigin-RevId: 258787744 --- .../android/exoplayer2/DefaultMediaClock.java | 11 +-- .../android/exoplayer2/ExoPlayerImpl.java | 27 +++++- .../exoplayer2/ExoPlayerImplInternal.java | 28 ++++-- .../com/google/android/exoplayer2/Player.java | 11 +-- .../android/exoplayer2/audio/AudioSink.java | 7 +- .../exoplayer2/audio/DefaultAudioSink.java | 19 ++-- .../audio/MediaCodecAudioRenderer.java | 4 +- .../audio/SimpleDecoderAudioRenderer.java | 4 +- .../android/exoplayer2/util/MediaClock.java | 9 +- .../exoplayer2/util/StandaloneMediaClock.java | 3 +- .../exoplayer2/DefaultMediaClockTest.java | 48 ++-------- .../android/exoplayer2/ExoPlayerTest.java | 93 ++++++++++++++++++- 12 files changed, 173 insertions(+), 91 deletions(-) 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 bcec6426d6..410dffd558 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,12 +32,12 @@ 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; @@ -141,13 +141,12 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } @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; } @Override 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 f380af968c..3eed66402d 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 @@ -69,6 +69,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private boolean hasPendingPrepare; private boolean hasPendingSeek; private boolean foregroundMode; + private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; @Nullable private ExoPlaybackException playbackError; @@ -336,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 @@ -560,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; @@ -576,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, 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 38cdb57fc8..738a30fad1 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 @@ -297,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. @@ -358,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); @@ -783,6 +782,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) { @@ -1663,9 +1664,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) { @@ -1820,6 +1825,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; 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 4e062dcb5e..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 @@ -761,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/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/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index e3f753958e..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 @@ -825,17 +825,12 @@ 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 @@ -847,12 +842,16 @@ public final class DefaultAudioSink implements AudioSink { 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 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 7e889097bc..b965f4ef68 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 @@ -648,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 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 ef0207517a..b17fa75181 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 @@ -517,8 +517,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 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/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/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index be0f7f55c7..c42edb32ae 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; @@ -116,15 +115,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 @@ -137,24 +135,9 @@ public class DefaultMediaClockTest { @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 @@ -210,19 +193,18 @@ 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.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.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verifyNoMoreInteractions(listener); } @Test @@ -231,19 +213,8 @@ 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.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test @@ -418,11 +389,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 39046b52ce..d8ba3bcbda 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 @@ -223,9 +223,7 @@ public final class ExoPlayerTest { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return PlaybackParameters.DEFAULT; - } + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} @Override public PlaybackParameters getPlaybackParameters() { @@ -2641,6 +2639,95 @@ public final class ExoPlayerTest { 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) { From 421f6e0303d324010a7e2c32c6b5fa9fe36987be Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jul 2019 18:37:55 +0100 Subject: [PATCH 219/807] Add AV1 HDR profile recognition Recognize AV1ProfileMain10HDR when getting codec profile and level. PiperOrigin-RevId: 258799457 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 14 ++-- .../mediacodec/MediaCodecUtilTest.java | 68 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) 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 df5ca05972..6a967a359b 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; @@ -255,7 +257,7 @@ public final class MediaCodecUtil { case CODEC_ID_DVH1: return getDolbyVisionProfileAndLevel(format.codecs, parts); case CODEC_ID_AV01: - return getAv1ProfileAndLevel(format.codecs, parts); + return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo); case CODEC_ID_MP4A: return getAacCodecProfileAndLevel(format.codecs, parts); default: @@ -686,7 +688,8 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } - private static Pair getAv1ProfileAndLevel(String codec, String[] parts) { + 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; @@ -703,8 +706,6 @@ public final class MediaCodecUtil { return null; } - // TODO: Recognize HDR profiles. Currently, the profile is assumed to be either Main8 or Main10. - // See [Internal: b/124435216]. if (profileInteger != 0) { Log.w(TAG, "Unknown AV1 profile: " + profileInteger); return null; @@ -716,6 +717,11 @@ public final class MediaCodecUtil { 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; } 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 c485ff49f6..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,8 +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; @@ -78,6 +80,68 @@ public final class MediaCodecUtilTest { 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. @@ -135,6 +199,10 @@ public final class MediaCodecUtilTest { /* 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); From 8b554dc30a8d17f1f9d64b72e079e5c2bb82c31e Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Fri, 19 Jul 2019 08:38:47 +0200 Subject: [PATCH 220/807] Improve code readability and fix an issue with text tracks that should not be selected --- .../trackselection/DefaultTrackSelector.java | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) 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 b5d282f8a7..b830fa5b78 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 @@ -2076,7 +2076,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); - if ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0) { + if (trackScore.isWithinConstraints + && ((selectedTrackScore == null) || trackScore.compareTo(selectedTrackScore) > 0)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2514,8 +2515,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final int preferredLanguageScore; private final int selectedAudioLanguageScore; private final boolean trackHasNoLanguage; - private final boolean selectUndeterminedTextLanguage; - private final boolean stringDefinesNoLang; + private final boolean hasLanguageMatch; + private final boolean hasSelectedAudioLanguageMatch; + private final boolean isWithinConstraints; public TextTrackScore( Format format, @@ -2525,13 +2527,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { isWithinRendererCapabilities = isSupported(trackFormatSupport, false); int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; - isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredTextLanguage); selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage); trackHasNoLanguage = formatHasNoLanguage(format); - selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage; - stringDefinesNoLang = stringDefinesNoLanguage(selectedAudioLanguage); + hasLanguageMatch = preferredLanguageScore > 0 + || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage); + hasSelectedAudioLanguageMatch = (selectedAudioLanguageScore > 0) + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)); + isWithinConstraints = + (hasLanguageMatch || isDefault || (isForced && hasSelectedAudioLanguageMatch)); } /** @@ -2546,34 +2552,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } - if ((this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) == - (other.preferredLanguageScore > 0 || (other.selectUndeterminedTextLanguage && other.trackHasNoLanguage))) { - if (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage - && this.trackHasNoLanguage)) { - if (this.isDefault != other.isDefault) { - return this.isDefault ? 1 : -1; - } - if (this.isForced != other.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. - return !this.isForced ? 1 : -1; - } - return (this.preferredLanguageScore > other.preferredLanguageScore) ? 1 : -1; - } else { - if (this.isDefault != other.isDefault) { - return this.isDefault ? 1 : -1; - } - if ((this.isForced && (this.selectedAudioLanguageScore > 0 || (this.trackHasNoLanguage && this.stringDefinesNoLang))) != - (other.isForced && (other.selectedAudioLanguageScore > 0 || (other.trackHasNoLanguage && other.stringDefinesNoLang)))) { - return (this.isForced && (this.selectedAudioLanguageScore > 0 - || (this.trackHasNoLanguage && this.stringDefinesNoLang))) ? 1 : -1; - } - // Track should not be selected. - return -1; + if (this.hasLanguageMatch != other.hasLanguageMatch) { + return this.hasLanguageMatch ? 1 : -1; + } + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.hasLanguageMatch) { + if (this.isForced != other.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. + return !this.isForced ? 1 : -1; } + return this.preferredLanguageScore - other.preferredLanguageScore; } else { - return (this.preferredLanguageScore > 0 || (this.selectUndeterminedTextLanguage && this.trackHasNoLanguage)) ? 1 : -1; + if ((this.isForced && this.hasSelectedAudioLanguageMatch) != + (other.isForced && other.hasSelectedAudioLanguageMatch)) { + return (this.isForced && this.hasSelectedAudioLanguageMatch) ? 1 : -1; + } + return 0; } } } From 3a53543a9af343e23d34af6fdfa551d9baf05464 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 22 Jul 2019 19:27:05 +0100 Subject: [PATCH 221/807] Move HLS DrmInitData adjustment to the writing side + Emulates what's done for ID3 stripping. + Also avoid a copy if fields will not change because of the copy. PiperOrigin-RevId: 259369101 --- .../com/google/android/exoplayer2/Format.java | 40 +++++-------------- .../source/DecryptableSampleQueueReader.java | 20 ---------- .../source/hls/HlsSampleStreamWrapper.java | 19 +++++++-- 3 files changed, 25 insertions(+), 54 deletions(-) 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 df01df1708..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 @@ -1291,39 +1291,19 @@ public final class Format implements Parcelable { } 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, - exoMediaCryptoType); + 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, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java index b0b10d4e98..365a48cadf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java @@ -27,8 +27,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -41,7 +39,6 @@ public final class DecryptableSampleQueueReader { private final DrmSessionManager sessionManager; private final FormatHolder formatHolder; private final boolean playClearSamplesWithoutKeys; - private final HashMap overridingDrmInitDatas; private @MonotonicNonNull Format currentFormat; @Nullable private DrmSession currentSession; @@ -58,19 +55,6 @@ public final class DecryptableSampleQueueReader { formatHolder = new FormatHolder(); playClearSamplesWithoutKeys = (sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0; - overridingDrmInitDatas = new HashMap<>(); - } - - /** - * Given a mapping from {@link DrmInitData#schemeType} to {@link DrmInitData}, overrides any - * {@link DrmInitData} read from the upstream {@link SampleQueue} whose {@link - * DrmInitData#schemeType} is a key in the mapping to use the corresponding {@link DrmInitData} - * value. If {@code overridingDrmInitDatas} does not contain a mapping for the upstream {@link - * DrmInitData#schemeType}, the upstream {@link DrmInitData} is used. - */ - public void setOverridingDrmInitDatas(Map overridingDrmInitDatas) { - this.overridingDrmInitDatas.clear(); - this.overridingDrmInitDatas.putAll(overridingDrmInitDatas); } /** Releases any resources acquired by this reader. */ @@ -192,10 +176,6 @@ public final class DecryptableSampleQueueReader { DrmSession previousSession = currentSession; DrmInitData drmInitData = currentFormat.drmInitData; if (drmInitData != null) { - DrmInitData overridingDrmInitData = overridingDrmInitDatas.get(drmInitData.schemeType); - if (overridingDrmInitData != null) { - drmInitData = overridingDrmInitData; - } currentSession = sessionManager.acquireSession(Assertions.checkNotNull(Looper.myLooper()), drmInitData); } else { 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 079852c4d4..360a7d6f72 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 @@ -828,7 +828,7 @@ import java.util.Set; return createDummyTrackOutput(id, type); } } - SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator); + SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); @@ -1170,15 +1170,26 @@ import java.util.Set; 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))); } /** From e6bafec41842a30495018231416a72371417afa7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 23 Jul 2019 08:02:24 +0100 Subject: [PATCH 222/807] Deduplicate ID3 header constants PiperOrigin-RevId: 259479785 --- .../android/exoplayer2/extractor/Id3Peeker.java | 2 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 7 ++++--- .../exoplayer2/extractor/ts/Ac4Extractor.java | 8 ++++---- .../exoplayer2/extractor/ts/AdtsExtractor.java | 8 +++++--- .../android/exoplayer2/extractor/ts/Id3Reader.java | 13 ++++++------- 5 files changed, 20 insertions(+), 18 deletions(-) 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/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 2f46744ea0..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; @@ -44,7 +46,6 @@ 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 = 0x00494433; private final Ac3Reader reader; private final ParsableByteArray sampleData; @@ -62,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; 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 2e8dcd952b..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; @@ -52,8 +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 = 0x00494433; - private final Ac4Reader reader; private final ParsableByteArray sampleData; @@ -70,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; 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 3dd88fbb51..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; @@ -65,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 = 0x00494433; /** * 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. @@ -109,7 +110,8 @@ public final class AdtsExtractor implements Extractor { 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); } @@ -209,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/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. From 2a8cf2f5efa97bd4bcdc2a25cd93fb17f0d2d922 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 23 Jul 2019 13:53:59 +0100 Subject: [PATCH 223/807] Plumb DrmSessionManager into HlsMediaSource PiperOrigin-RevId: 259520431 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 8 +++ .../exoplayer2/source/hls/HlsMediaSource.java | 23 +++++++ .../source/hls/HlsSampleStream.java | 5 +- .../source/hls/HlsSampleStreamWrapper.java | 63 ++++++++++++++----- .../source/hls/HlsMediaPeriodTest.java | 2 + 6 files changed, 87 insertions(+), 18 deletions(-) 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 249223bff5..1f60224b28 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 @@ -484,7 +484,9 @@ public class PlayerActivity extends AppCompatActivity .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) .setDrmSessionManager(drmSessionManager) 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 39b49da402..e21827557a 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; @@ -63,6 +65,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; @Nullable private final TransferListener mediaTransferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -91,6 +94,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 +109,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 +120,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; @@ -735,6 +742,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 12c6a8ee72..877b6d486e 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(); } @@ -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,6 +316,7 @@ 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; @@ -310,6 +330,7 @@ public final class HlsMediaSource extends BaseMediaSource 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; @@ -352,6 +374,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistTracker, dataSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, allocator, 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 360a7d6f72..c8c1b8f566 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; @@ -94,6 +97,7 @@ import java.util.Set; 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; @@ -107,6 +111,7 @@ import java.util.Set; private final Map overridingDrmInitData; private SampleQueue[] sampleQueues; + private DecryptableSampleQueueReader[] sampleQueueReaders; private int[] sampleQueueTrackIds; private boolean audioSampleQueueMappingDone; private int audioSampleQueueIndex; @@ -154,6 +159,8 @@ import java.util.Set; * @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. */ @@ -165,6 +172,7 @@ import java.util.Set; Allocator allocator, long positionUs, Format muxedAudioFormat, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.trackType = trackType; @@ -173,6 +181,7 @@ import java.util.Set; this.overridingDrmInitData = overridingDrmInitData; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); @@ -181,6 +190,7 @@ import java.util.Set; 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<>(); @@ -211,7 +221,7 @@ import java.util.Set; public void prepareWithMasterPlaylistInfo( TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { prepared = true; - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); optionalTrackGroups = new HashSet<>(); for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); @@ -438,6 +448,9 @@ import java.util.Set; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(this); handler.removeCallbacksAndMessages(null); @@ -448,6 +461,9 @@ import java.util.Set; @Override public void onLoaderReleased() { resetSampleQueues(); + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } public void setIsTimestampMaster(boolean isTimestampMaster) { @@ -461,7 +477,12 @@ import java.util.Set; // 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 { @@ -494,13 +515,8 @@ import java.util.Set; } int result = - sampleQueues[sampleQueueIndex].read( - formatHolder, - buffer, - requireFormat, - /* allowOnlyClearBuffers= */ false, - loadingFinished, - lastSeekPositionUs); + sampleQueueReaders[sampleQueueIndex].read( + formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (sampleQueueIndex == primarySampleQueueIndex) { @@ -516,12 +532,6 @@ import java.util.Set; : 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; @@ -836,6 +846,9 @@ import java.util.Set; 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; @@ -1048,11 +1061,29 @@ import java.util.Set; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); Assertions.checkState(optionalTrackGroups == null); 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() { return mediaChunks.get(mediaChunks.size() - 1); } 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 f389944670..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 @@ -22,6 +22,7 @@ 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( From 3c3777d4debacbd714acde2e264470aee6eba445 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 23 Jul 2019 14:20:00 +0100 Subject: [PATCH 224/807] Fix release of DecryptableSampleQueueReaders in ProgressiveMediaPeriod PiperOrigin-RevId: 259523450 --- .../exoplayer2/source/ProgressiveMediaPeriod.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 83145d04b0..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 @@ -200,9 +200,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } - } - for (DecryptableSampleQueueReader reader : sampleQueueReaders) { - reader.release(); + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); @@ -216,6 +216,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } extractorHolder.release(); } From e5b3c32c983bb5b4c328a6793b50f167104afef8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 23 Jul 2019 15:07:43 +0100 Subject: [PATCH 225/807] Remove DrmSessionManager from Renderer creation in the main demo app PiperOrigin-RevId: 259529691 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 1f60224b28..40b1a94991 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 @@ -416,8 +416,7 @@ public class PlayerActivity extends AppCompatActivity 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)); From 39574b5a614b134ae65b6d95175ae659daa0eddc Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 23 Jul 2019 15:33:29 +0100 Subject: [PATCH 226/807] Make one of the ExoPlayerTest tests more sensible. Some variables were defined although they are the default and other things were set-up in a non-sensible way, e.g. asserting that audio is selected although no audio renderer is available, or using unset duration for everything. PiperOrigin-RevId: 259532782 --- .../android/exoplayer2/ExoPlayerTest.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) 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 d8ba3bcbda..d5b0b2c667 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 @@ -51,7 +51,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; @@ -2253,17 +2252,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, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, 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() @@ -2275,8 +2272,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() { From 2c318d7b8472917a11930659fa1afa1bdd057cf7 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Jul 2019 19:59:55 +0100 Subject: [PATCH 227/807] Cast: Remove obsolete flavor dimension PiperOrigin-RevId: 259582498 --- demos/cast/build.gradle | 11 ----------- demos/cast/src/main/AndroidManifest.xml | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 03a54947cf..85e60f2796 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -47,17 +47,6 @@ android { // The demo app isn't indexed and doesn't have translations. disable 'GoogleAppIndexingWarning','MissingTranslation' } - - flavorDimensions "receiver" - - productFlavors { - defaultCast { - dimension "receiver" - manifestPlaceholders = - [castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"] - } - } - } dependencies { diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 856b0b1235..dbfdd833f6 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:largeHeap="true" android:allowBackup="false"> + android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> Date: Tue, 23 Jul 2019 20:21:10 +0100 Subject: [PATCH 228/807] Cast extension: Remove unused parts of MediaItem PiperOrigin-RevId: 259586520 --- .../exoplayer2/castdemo/MainActivity.java | 16 +- .../exoplayer2/ext/cast/MediaItem.java | 140 +----------------- .../exoplayer2/ext/cast/MediaItemTest.java | 66 ++------- 3 files changed, 21 insertions(+), 201 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index c17c0a62ab..2adfb28632 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -51,8 +51,6 @@ import java.util.Collections; public class MainActivity extends AppCompatActivity implements OnClickListener, PlayerManager.Listener { - private final MediaItem.Builder mediaItemBuilder; - private PlayerView localPlayerView; private PlayerControlView castControlView; private PlayerManager playerManager; @@ -60,10 +58,6 @@ public class MainActivity extends AppCompatActivity private MediaQueueListAdapter mediaQueueListAdapter; private CastContext castContext; - public MainActivity() { - mediaItemBuilder = new MediaItem.Builder(); - } - // Activity lifecycle methods. @Override @@ -179,11 +173,11 @@ public class MainActivity extends AppCompatActivity sampleList.setOnItemClickListener( (parent, view, position, id) -> { DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - mediaItemBuilder - .clear() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType); + MediaItem.Builder mediaItemBuilder = + new MediaItem.Builder() + .setMedia(sample.uri) + .setTitle(sample.name) + .setMimeType(sample.mimeType); DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; if (drmConfiguration != null) { mediaItemBuilder.setDrmSchemes( 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..79f36f46d7 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,8 +17,6 @@ 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; @@ -26,8 +24,6 @@ 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. */ public final class MediaItem { @@ -35,25 +31,16 @@ 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; - /** Creates an builder with default field values. */ public Builder() { - clearInternal(); - } - - /** See {@link MediaItem#uuid}. */ - public Builder setUuid(UUID uuid) { - this.uuid = uuid; - return this; + title = ""; + media = UriBundle.EMPTY; + drmSchemes = Collections.emptyList(); + mimeType = ""; } /** See {@link MediaItem#title}. */ @@ -62,12 +49,6 @@ 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))); @@ -79,84 +60,26 @@ public final class MediaItem { 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(); - 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 = ""; - } } /** Bundles a resource's URI with headers to attach to any request to that URI. */ @@ -259,49 +182,20 @@ public final class MediaItem { } } - /** - * 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 title of the item. The default value is an empty string. */ public final String title; - /** A description for the item. The default value is an empty string. */ - public final String description; - /** * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}. */ public final UriBundle media; - /** - * 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. * @@ -320,49 +214,29 @@ public final class MediaItem { 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) + return title.equals(mediaItem.title) && media.equals(mediaItem.media) - && Util.areEqual(attachment, mediaItem.attachment) && drmSchemes.equals(mediaItem.drmSchemes) && mimeType.equals(mediaItem.mimeType); } @Override public int hashCode() { - int result = uuid.hashCode(); - result = 31 * result + title.hashCode(); - result = 31 * result + description.hashCode(); + int result = title.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(); 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/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..d21e57efd1 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 @@ -24,7 +24,6 @@ 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,32 +31,16 @@ 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") .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); } @@ -66,63 +49,32 @@ public class MediaItemTest { 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.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(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.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(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.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(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.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(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(); } From 5e88621ab00d1192c73f4dc792b8d16f136b93ad Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 Jul 2019 22:12:49 +0100 Subject: [PATCH 229/807] Make LibopusAudioRenderer non-final PiperOrigin-RevId: 259608495 --- .../android/exoplayer2/ext/opus/LibopusAudioRenderer.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 fe74f5c59c..b8b9598989 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 @@ -26,10 +26,8 @@ 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; From 7d2bfdfc626aab2e561d25818b5910ee738ebc1f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 24 Jul 2019 08:23:28 +0100 Subject: [PATCH 230/807] Add AudioFocusGain IntDef in AudioFocusManager PiperOrigin-RevId: 259687632 --- .../google/android/exoplayer2/audio/AudioFocusManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 2d65b64f36..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 @@ -105,9 +105,9 @@ public final class AudioFocusManager { private final PlayerControl playerControl; @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) { From e84d88e90f62d1755317cd692496bac58c5b07d3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 11:05:27 +0100 Subject: [PATCH 231/807] Simplify and improve text selection logic. This changes the logic in the following ways: - If no preferred language is matched, prefer better scores for the selected audio language. - If a preferred language is matched, always prefer the better match irrespective of default or forced flags. - If a preferred language score and the isForced flag is the same, prefer tracks with a better selected audio language match. PiperOrigin-RevId: 259707430 --- RELEASENOTES.md | 2 ++ .../trackselection/DefaultTrackSelector.java | 35 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5a9070ecf2..176c786682 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Add ability to specify a description when creating notification channels via ExoPlayer library classes. +* Improve text selection logic to always prefer the better language matches + over other selection parameters. ### 2.10.3 ### 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 c1c0c5cbc7..56eebfbee4 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 @@ -2536,9 +2536,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final boolean isWithinRendererCapabilities; private final boolean isDefault; - private final boolean isForced; + private final boolean hasPreferredIsForcedFlag; private final int preferredLanguageScore; - private final boolean isForcedAndSelectedAudioLanguage; + private final int selectedAudioLanguageScore; public TextTrackScore( Format format, @@ -2550,17 +2550,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 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; - int selectedAudioLanguageScore = + selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); - isForcedAndSelectedAudioLanguage = isForced && selectedAudioLanguageScore > 0; isWithinConstraints = - preferredLanguageScore > 0 || isDefault || isForcedAndSelectedAudioLanguage; + preferredLanguageScore > 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); } /** @@ -2575,25 +2579,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } - if ((this.preferredLanguageScore > 0) != (other.preferredLanguageScore > 0)) { - return this.preferredLanguageScore > 0 ? 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.preferredLanguageScore > 0) { - if (this.isForced != other.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. - return !this.isForced ? 1 : -1; - } - return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { + return this.hasPreferredIsForcedFlag ? 1 : -1; } - if (this.isForcedAndSelectedAudioLanguage != other.isForcedAndSelectedAudioLanguage) { - return this.isForcedAndSelectedAudioLanguage ? 1 : -1; - } - return 0; + return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); } } } From a0ca79abcc97f010cc829b05d9e98e5524aed450 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 12:17:05 +0100 Subject: [PATCH 232/807] Fix doc for preferred audio and text language. Both tags allow any BCP47 compliant code, not just the ISO 639-2/T ones. PiperOrigin-RevId: 259714587 --- .../TrackSelectionParameters.java | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) 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..81af551b68 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,9 +46,7 @@ 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; @@ -67,8 +63,6 @@ public class TrackSelectionParameters implements Parcelable { return this; } - // Text - /** * See {@link TrackSelectionParameters#preferredTextLanguage}. * @@ -117,15 +111,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 +156,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 +178,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 +186,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 +202,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); From 59331c3c887ce8d42b76aa2e1c46106028f862dc Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 15:34:07 +0100 Subject: [PATCH 233/807] Report mediaPeriodCreated/Released in MaskingMediaSource. Creating a period in MaskingMediaSource may result in delayed event reporting depending on when the actual period gets created. To avoid event reporting inaccuracies, report the mediaPeriodCreated and mediaPeriodReleased events directly. Issue:#5407 PiperOrigin-RevId: 259737170 --- .../source/CompositeMediaSource.java | 30 +++++++++++++++---- .../exoplayer2/source/MaskingMediaSource.java | 21 +++++++++++-- 2 files changed, 43 insertions(+), 8 deletions(-) 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 3672c304cc..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 @@ -96,11 +96,11 @@ public abstract class CompositeMediaSource extends BaseMediaSource { /** * Prepares a child source. * - *

          {@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the - * child source updates its timeline 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}. @@ -188,6 +188,18 @@ 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; @@ -215,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/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 1fca824910..d9dd83de4f 100644 --- 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 @@ -19,8 +19,10 @@ 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; @@ -37,6 +39,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { private MaskingTimeline timeline; @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher; private boolean hasStartedPreparing; private boolean isPrepared; @@ -96,6 +99,9 @@ public final class MaskingMediaSource extends CompositeMediaSource { // 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); @@ -107,7 +113,11 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); - unpreparedMaskingMediaPeriod = null; + if (mediaPeriod == unpreparedMaskingMediaPeriod) { + Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased(); + unpreparedMaskingMediaPeriodEventDispatcher = null; + unpreparedMaskingMediaPeriod = null; + } } @Override @@ -154,7 +164,6 @@ public final class MaskingMediaSource extends CompositeMediaSource { timeline = MaskingTimeline.createWithRealTimeline(newTimeline, periodUid); if (unpreparedMaskingMediaPeriod != null) { MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; - unpreparedMaskingMediaPeriod = null; maskingPeriod.overridePreparePositionUs(periodPositionUs); MediaPeriodId idInSource = maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); @@ -172,6 +181,14 @@ public final class MaskingMediaSource extends CompositeMediaSource { 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 From 074307dd4c4a591dd851afe3a7ce315505585ffc Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 24 Jul 2019 15:34:29 +0100 Subject: [PATCH 234/807] Improve knowledge of last playing period in AnalyticsCollector. We keep track of the last publicly known playing period to report it as part of events happening after the period has been released. In cases where a period briefly becomes the playing one and is released immediately afterwards, we currently don't save it as the "last known playing one". Improve that by saving an explicit reference. Issue:#5407 PiperOrigin-RevId: 259737218 --- .../android/exoplayer2/MediaPeriodQueue.java | 2 +- .../analytics/AnalyticsCollector.java | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) 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 0dacd4df30..2597cd9b3f 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 @@ -277,8 +277,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; } 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 de0f177342..091696f8bf 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 @@ -686,6 +686,7 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; + @Nullable private MediaPeriodInfo lastPlayingMediaPeriod; @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; @@ -780,7 +781,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. */ @@ -795,7 +796,7 @@ public class AnalyticsCollector readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported start of seek. */ @@ -806,7 +807,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. */ @@ -816,8 +817,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; } } @@ -835,6 +837,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; } @@ -843,12 +848,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); From 1b9e2497ff0fc451f31a155424b17347047187af Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Thu, 25 Jul 2019 11:42:11 +0100 Subject: [PATCH 235/807] Add comment about AV1 levels PiperOrigin-RevId: 259918196 --- .../google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 2 ++ 1 file changed, 2 insertions(+) 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 6a967a359b..e8fead61ae 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 @@ -1060,6 +1060,8 @@ 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); From 596be3b71ba1e37fe02dd82b13d3042d97711ff9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 25 Jul 2019 22:32:04 +0100 Subject: [PATCH 236/807] Cast: Simplify MediaItem/Sample to a single MediaItem class PiperOrigin-RevId: 260021990 --- .../android/exoplayer2/castdemo/DemoUtil.java | 88 ++----- .../exoplayer2/castdemo/MainActivity.java | 31 +-- .../exoplayer2/castdemo/PlayerManager.java | 38 +-- .../exoplayer2/ext/cast/MediaItem.java | 225 ++++++------------ .../exoplayer2/ext/cast/MediaItemTest.java | 56 ++--- 5 files changed, 148 insertions(+), 290 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 2d5a5f0ccf..91ea0c92e2 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -15,97 +15,49 @@ */ package com.google.android.exoplayer2.castdemo; -import androidx.annotation.Nullable; +import android.net.Uri; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ /* package */ final class DemoUtil { - /** Represents a media sample. */ - public static final class Sample { - - /** The URI of the media content. */ - public final String uri; - /** The name of the sample. */ - public final String name; - /** The mime type of the sample media content. */ - public final String mimeType; - /** Data to configure DRM license acquisition. May be null if content is not DRM-protected. */ - @Nullable public final DrmConfiguration drmConfiguration; - - public Sample(String uri, String name, String mimeType) { - this(uri, name, mimeType, /* drmConfiguration= */ null); - } - - public Sample( - String uri, String name, String mimeType, @Nullable DrmConfiguration drmConfiguration) { - this.uri = uri; - this.name = name; - this.mimeType = mimeType; - this.drmConfiguration = drmConfiguration; - } - - @Override - public String toString() { - return name; - } - } - - /** Holds information required to play DRM-protected content. */ - public static final class DrmConfiguration { - - /** The {@link UUID} of the DRM scheme that protects the content. */ - public final UUID drmSchemeUuid; - /** - * The URI from which players should obtain DRM licenses. May be null if the license server URI - * is provided as part of the media. - */ - @Nullable public final String licenseServerUri; - /** HTTP request headers to include the in DRM license requests. */ - public final Map httpRequestHeaders; - - public DrmConfiguration( - UUID drmSchemeUuid, - @Nullable String licenseServerUri, - Map httpRequestHeaders) { - this.drmSchemeUuid = drmSchemeUuid; - this.licenseServerUri = licenseServerUri; - this.httpRequestHeaders = httpRequestHeaders; - } - } - public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; /** The list of samples available in the cast demo app. */ - public static final List SAMPLES; + public static final List SAMPLES; static { // App samples. - ArrayList samples = new ArrayList<>(); + ArrayList samples = new ArrayList<>(); // Clear content. samples.add( - new Sample( - "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "Clear DASH: Tears", - MIME_TYPE_DASH)); + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") + .setTitle("Clear DASH: Tears") + .setMimeType(MIME_TYPE_DASH) + .build()); samples.add( - new Sample( - "https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8", - "Clear HLS: Angel one", - MIME_TYPE_HLS)); + new MediaItem.Builder() + .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8") + .setTitle("Clear HLS: Angel one") + .setMimeType(MIME_TYPE_HLS) + .build()); samples.add( - new Sample( - "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); + new MediaItem.Builder() + .setUri("https://html5demos.com/assets/dizzy.mp4") + .setTitle("Clear MP4: Dizzy") + .setMimeType(MIME_TYPE_VIDEO_MP4) + .build()); SAMPLES = Collections.unmodifiableList(samples); } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 2adfb28632..1a7f28cd77 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; @@ -42,7 +41,6 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; -import java.util.Collections; /** * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's @@ -172,25 +170,7 @@ public class MainActivity extends AppCompatActivity sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setOnItemClickListener( (parent, view, position, id) -> { - DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - MediaItem.Builder mediaItemBuilder = - new MediaItem.Builder() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType); - DemoUtil.DrmConfiguration drmConfiguration = sample.drmConfiguration; - if (drmConfiguration != null) { - mediaItemBuilder.setDrmSchemes( - Collections.singletonList( - new MediaItem.DrmScheme( - drmConfiguration.drmSchemeUuid, - new MediaItem.UriBundle( - drmConfiguration.licenseServerUri != null - ? Uri.parse(drmConfiguration.licenseServerUri) - : Uri.EMPTY, - drmConfiguration.httpRequestHeaders)))); - } - playerManager.addItem(mediaItemBuilder.build()); + playerManager.addItem(DemoUtil.SAMPLES.get(position)); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; @@ -213,8 +193,10 @@ public class MainActivity extends AppCompatActivity TextView view = holder.textView; view.setText(holder.item.title); // TODO: Solve coloring using the theme's ColorStateList. - view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), - position == playerManager.getCurrentItemIndex() ? 255 : 100)); + view.setTextColor( + ColorUtils.setAlphaComponent( + view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); } @Override @@ -294,11 +276,10 @@ public class MainActivity extends AppCompatActivity } } - private static final class SampleListAdapter extends ArrayAdapter { + private static final class SampleListAdapter extends ArrayAdapter { public SampleListAdapter(Context context) { super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } } - } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index d2a1ca0860..b877ac7593 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -44,7 +44,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; @@ -368,8 +367,12 @@ import org.json.JSONObject; } private static MediaSource buildMediaSource(MediaItem item) { - Uri uri = item.media.uri; - switch (item.mimeType) { + Uri uri = item.uri; + String mimeType = item.mimeType; + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + switch (mimeType) { case DemoUtil.MIME_TYPE_SS: return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_DASH: @@ -379,7 +382,7 @@ import org.json.JSONObject; case DemoUtil.MIME_TYPE_VIDEO_MP4: return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); default: - throw new IllegalStateException("Unsupported type: " + item.mimeType); + throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } } @@ -387,18 +390,18 @@ import org.json.JSONObject; MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(item.media.uri.toString()) + new MediaInfo.Builder(item.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) .setMetadata(movieMetadata); - if (!item.drmSchemes.isEmpty()) { - MediaItem.DrmScheme scheme = item.drmSchemes.get(0); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null) { try { // This configuration is only intended for testing and should *not* be used in production // environments. See comment in the Cast Demo app's options provider. - JSONObject drmConfiguration = getDrmConfigurationJson(scheme); - if (drmConfiguration != null) { - mediaInfoBuilder.setCustomData(drmConfiguration); + JSONObject drmConfigurationJson = getDrmConfigurationJson(drmConfiguration); + if (drmConfigurationJson != null) { + mediaInfoBuilder.setCustomData(drmConfigurationJson); } } catch (JSONException e) { throw new RuntimeException(e); @@ -408,24 +411,23 @@ import org.json.JSONObject; } @Nullable - private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme) + private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration) throws JSONException { String drmScheme; - if (C.WIDEVINE_UUID.equals(scheme.uuid)) { + if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) { drmScheme = "widevine"; - } else if (C.PLAYREADY_UUID.equals(scheme.uuid)) { + } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { drmScheme = "playready"; } else { return null; } - MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer); JSONObject exoplayerConfig = new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); - if (!licenseServer.uri.equals(Uri.EMPTY)) { - exoplayerConfig.put("licenseUrl", licenseServer.uri.toString()); + if (drmConfiguration.licenseUri != null) { + exoplayerConfig.put("licenseUrl", drmConfiguration.licenseUri); } - if (!licenseServer.requestHeaders.isEmpty()) { - exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders)); + if (!drmConfiguration.requestHeaders.isEmpty()) { + exoplayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); } return new JSONObject().put("exoPlayerConfig", exoplayerConfig); } 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 79f36f46d7..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,30 +17,32 @@ package com.google.android.exoplayer2.ext.cast; 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.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.UUID; -/** 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 { - private String title; - private MediaItem.UriBundle media; - private List drmSchemes; - private String mimeType; + @Nullable private Uri uri; + @Nullable private String title; + @Nullable private String mimeType; + @Nullable private DrmConfiguration drmConfiguration; - public Builder() { - title = ""; - media = UriBundle.EMPTY; - drmSchemes = Collections.emptyList(); - mimeType = ""; + /** See {@link MediaItem#uri}. */ + public Builder setUri(String uri) { + return setUri(Uri.parse(uri)); + } + + /** See {@link MediaItem#uri}. */ + public Builder setUri(Uri uri) { + this.uri = uri; + return this; } /** See {@link MediaItem#title}. */ @@ -49,194 +51,125 @@ public final class MediaItem { 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#drmSchemes}. */ - public Builder setDrmSchemes(List drmSchemes) { - this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes)); - return this; - } - /** See {@link MediaItem#mimeType}. */ public Builder setMimeType(String mimeType) { this.mimeType = mimeType; return this; } + /** 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. */ public MediaItem build() { - return new MediaItem( - title, - media, - drmSchemes, - 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; } } - /** The title of the item. The default value is an empty string. */ - public final String title; + /** The media {@link Uri}. */ + public final Uri uri; - /** - * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}. - */ - public final UriBundle media; + /** The title of the item, or {@code null} if unspecified. */ + @Nullable public final String title; - /** - * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The - * default value is an empty list. - */ - public final List drmSchemes; + /** The mime type for the media, or {@code null} if unspecified. */ + @Nullable public final String mimeType; - /** - * 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; + /** Optional {@link DrmConfiguration} for the media. */ + @Nullable public final DrmConfiguration drmConfiguration; - // 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 title.equals(mediaItem.title) - && media.equals(mediaItem.media) - && 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 = title.hashCode(); - result = 31 * result + media.hashCode(); - result = 31 * result + drmSchemes.hashCode(); - 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( - String title, - UriBundle media, - List drmSchemes, - String mimeType) { - this.title = title; - this.media = media; - this.drmSchemes = drmSchemes; - this.mimeType = mimeType; - } } 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 d21e57efd1..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,9 +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 org.junit.Test; import org.junit.runner.RunWith; @@ -36,7 +34,7 @@ public class MediaItemTest { MediaItem.Builder builder = new MediaItem.Builder(); MediaItem item1 = builder - .setMedia("http://example.com") + .setUri(Uri.parse("http://example.com")) .setTitle("title") .setMimeType(MimeTypes.AUDIO_MP4) .build(); @@ -44,19 +42,20 @@ public class MediaItemTest { assertThat(item1).isEqualTo(item2); } - @Test - public void buildMediaItem_assertDefaultValues() { - assertDefaultValues(new MediaItem.Builder().build()); - } - @Test public void equals_withEqualDrmSchemes_returnsTrue() { MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder1.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + builder1 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(1)) + .build(); MediaItem.Builder builder2 = new MediaItem.Builder(); MediaItem mediaItem2 = - builder2.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + builder2 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(1)) + .build(); assertThat(mediaItem1).isEqualTo(mediaItem2); } @@ -64,33 +63,24 @@ public class MediaItemTest { public void equals_withDifferentDrmRequestHeaders_returnsFalse() { MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder1.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(1)).build(); + builder1 + .setUri(Uri.parse("www.google.com")) + .setDrmConfiguration(buildDrmConfiguration(1)) + .build(); MediaItem.Builder builder2 = new MediaItem.Builder(); MediaItem mediaItem2 = - builder2.setMedia("www.google.com").setDrmSchemes(createDummyDrmSchemes(2)).build(); + 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.media.uri).isEqualTo(Uri.EMPTY); - assertThat(item.drmSchemes).isEmpty(); - 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); } } From 9c41bcfe2445bc188b83fe354a7262bde4294b94 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Jul 2019 16:08:56 +0100 Subject: [PATCH 237/807] Add A10-70L to output surface workaround Issue: #6222 PiperOrigin-RevId: 260146226 --- .../google/android/exoplayer2/video/MediaCodecVideoRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 6e3114d1b1..24524d057d 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 @@ -1487,6 +1487,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "1713": case "1714": case "A10-70F": + case "A10-70L": case "A1601": case "A2016a40": case "A7000-a": From 6f7b765a1cf9ba78bf5c1c793d03fc2c754c2ede Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 Jul 2019 17:20:45 +0100 Subject: [PATCH 238/807] Fix handling of channel count changes with speed adjustment When using speed adjustment it was possible for playback to get stuck at a period transition when the channel count changed: SonicAudioProcessor would be drained at the point of the period transition in preparation for creating a new AudioTrack with the new channel count, but during draining the incorrect (new) channel count was used to calculate the output buffer size for pending data from Sonic. This meant that, for example, if the channel count changed from stereo to mono we could have an output buffer size that stored an non-integer number of audio frames, and in turn this would cause writing to the AudioTrack to get stuck as the AudioTrack would prevent writing a partial audio frame. Use Sonic's current channel count when draining output to fix the issue. PiperOrigin-RevId: 260156541 --- .../java/com/google/android/exoplayer2/audio/Sonic.java | 7 ++++--- .../android/exoplayer2/audio/SonicAudioProcessor.java | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) 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 From 09835c454bfb9a5b542290a4028472bb1d53b378 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 Jul 2019 18:07:02 +0100 Subject: [PATCH 239/807] Bump version to 2.10.4 PiperOrigin-RevId: 260164426 --- RELEASENOTES.md | 24 +++++++++++-------- constants.gradle | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 176c786682..747436a69d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,7 +5,6 @@ * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. -* Offline: Add `Scheduler` implementation that uses `WorkManager`. * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, @@ -19,23 +18,28 @@ `SourceInfoRefreshListener` anymore. Instead make it accessible through `Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename `SourceInfoRefreshListener` to `MediaSourceCaller`. -* Flac extension: Parse `VORBIS_COMMENT` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Set `compileSdkVersion` to 29 to use Android Q APIs. * Add `enable` and `disable` methods to `MediaSource` to improve resource management in playlists. -* Fix issue where initial seek positions get ignored when playing a preroll ad. -* Fix `DataSchemeDataSource` re-opening and range requests - ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Improve text selection logic to always prefer the better language matches + over other selection parameters. + +### 2.10.4 ### + +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of keeping the original ([#6153](https://github.com/google/ExoPlayer/issues/6153)). -* Add ability to specify a description when creating notification channels via - ExoPlayer library classes. -* Improve text selection logic to always prefer the better language matches - over other selection parameters. +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Flac extension: Parse `VORBIS_COMMENT` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). ### 2.10.3 ### diff --git a/constants.gradle b/constants.gradle index c8136ea471..aba52817bc 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.3' - releaseVersionCode = 2010003 + releaseVersion = '2.10.4' + releaseVersionCode = 2010004 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 29 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 190f4de5a6..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.3"; + 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.3"; + 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 = 2010003; + public static final int VERSION_INT = 2010004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From ea64ecf2c40854deed3120d2fb38b68f67ec516d Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Mon, 29 Jul 2019 14:24:42 +0530 Subject: [PATCH 240/807] Parse Picture Metadata in FLAC --- .../exoplayer2/ext/flac/FlacExtractor.java | 4 +- extensions/flac/src/main/jni/flac_jni.cc | 58 ++++++- extensions/flac/src/main/jni/flac_parser.cc | 19 +++ .../flac/src/main/jni/include/flac_parser.h | 21 +++ .../metadata/flac/PictureFrame.java | 154 ++++++++++++++++++ .../{vorbis => flac}/VorbisComment.java | 2 +- .../exoplayer2/util/FlacStreamMetadata.java | 16 +- .../exoplayer2/metadata/flac/PictureTest.java | 43 +++++ .../{vorbis => flac}/VorbisCommentTest.java | 2 +- .../util/FlacStreamMetadataTest.java | 18 +- .../android/exoplayer2/ui/PlayerView.java | 22 ++- 11 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java rename library/core/src/main/java/com/google/android/exoplayer2/metadata/{vorbis => flac}/VorbisComment.java (97%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java rename library/core/src/test/java/com/google/android/exoplayer2/metadata/{vorbis => flac}/VorbisCommentTest.java (96%) 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 151875c2c5..9f79f09117 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 @@ -229,8 +229,8 @@ public final class FlacExtractor implements Extractor { binarySearchSeeker = outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamMetadata.vorbisComments != null) { - metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.flacMetadata != null) { + metadata = streamMetadata.flacMetadata.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamMetadata, metadata, trackOutput); outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 4ba071e1ca..4ccd24781b 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -102,10 +102,10 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { 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->isVorbisCommentsValid()) { - jmethodID arrayListAddMethod = - env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); std::vector vorbisComments = context->parser->getVorbisComments(); for (std::vector::const_iterator vorbisComment = @@ -117,6 +117,39 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { } } + jobject pictureList = env->NewObject(arrayListClass, arrayListConstructor); + bool picValid = context->parser->isPicValid(); + if (picValid) { + std::vector pictures = context->parser->getPictures(); + jclass flacPictureFrameClass = env->FindClass( + "com/google/android/exoplayer2/metadata/flac/PictureFrame"); + jmethodID flacPictureFrameConstructor = env->GetMethodID( + flacPictureFrameClass, "", + "(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 picArr = env->NewByteArray(picture->data.size()); + env->SetByteArrayRegion(picArr, 0, picture->data.size(), + (signed char *)&picture->data[0]); + jobject flacPictureFrame = env->NewObject(flacPictureFrameClass, + flacPictureFrameConstructor, + picture->type, + mimeType, + description, + picture->width, + picture->height, + picture->depth, + picture->colors, + picArr); + env->CallBooleanMethod(pictureList, arrayListAddMethod, flacPictureFrame); + env->DeleteLocalRef(mimeType); + env->DeleteLocalRef(description); + env->DeleteLocalRef(picArr); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); @@ -124,14 +157,21 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { "com/google/android/exoplayer2/util/" "FlacStreamMetadata"); jmethodID flacStreamMetadataConstructor = env->GetMethodID( - flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;)V"); + flacStreamMetadataClass, "", + "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); - 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); + jobject streamMetaData = 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, pictureList); + return streamMetaData; } 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 b2d074252d..fafc254482 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -191,6 +191,22 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); } break; + case FLAC__METADATA_TYPE_PICTURE: + { + const FLAC__StreamMetadata_Picture *pic = &metadata->data.picture; + flacPicture picture; + picture.mimeType.assign(std::string(pic->mime_type)); + picture.description.assign(std::string((char *)pic->description)); + picture.data.assign(pic->data, pic->data + pic->data_length); + picture.width = pic->width; + picture.height = pic->height; + picture.depth = pic->depth; + picture.colors = pic->colors; + picture.type = pic->type; + mPictures.push_back(picture); + mPicValid = true; + break; + } default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -253,6 +269,7 @@ FLACParser::FLACParser(DataSource *source) mEOF(false), mStreamInfoValid(false), mVorbisCommentsValid(false), + mPicValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -288,6 +305,8 @@ bool FLACParser::init() { 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 f09a22e951..f1d175b94f 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -30,6 +30,17 @@ typedef int status_t; +typedef struct { + int type; + std::string mimeType; + std::string description; + FLAC__uint32 width; + FLAC__uint32 height; + FLAC__uint32 depth; + FLAC__uint32 colors; + std::vector data; +} flacPicture; + class FLACParser { public: FLACParser(DataSource *source); @@ -54,6 +65,10 @@ class FLACParser { return mVorbisComments; } + bool isPicValid() const { return mPicValid; } + + const std::vector& getPictures() const { return mPictures; } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -82,7 +97,9 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentsValid = false; + mPicValid = false; mVorbisComments.clear(); + mPictures.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -132,6 +149,10 @@ class FLACParser { std::vector mVorbisComments; bool mVorbisCommentsValid; + // cached when the PICTURE metadata is parsed by libFLAC + std::vector mPictures; + bool mPicValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; 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..fcf1fd6e58 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -0,0 +1,154 @@ +/* + * 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 in 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. */ + @Nullable public final String description; + /** The pixel width of the picture. */ + public final int width; + /** The pixel height of the picture. */ + 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, or 0 for non-indexed + * pictures. + */ + public final int colors; + /** The encoded picture data. */ + public final byte[] pictureData; + + public PictureFrame( + int pictureType, + String mimeType, + @Nullable 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 "FLAC Picture" + + "\nType: " + pictureType + + "\nMime Type: " + mimeType + + "\nDescription: " + description + + "\nWidth: " + width + + "\nHeight: " + height + + "\nDepth: " + depth + + "\nColors: " + colors; + } + + @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 != null ? mimeType.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + 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/vorbis/VorbisComment.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java similarity index 97% rename from library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java index b1951cbc13..9f44cdf393 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisComment.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata.vorbis; +package com.google.android.exoplayer2.metadata.flac; import static com.google.android.exoplayer2.util.Util.castNonNull; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 43fdda367e..48680b5095 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -18,7 +18,8 @@ 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.vorbis.VorbisComment; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; import java.util.List; @@ -35,7 +36,7 @@ public final class FlacStreamMetadata { public final int channels; public final int bitsPerSample; public final long totalSamples; - @Nullable public final Metadata vorbisComments; + @Nullable public final Metadata flacMetadata; private static final String SEPARATOR = "="; @@ -58,7 +59,7 @@ public final class FlacStreamMetadata { this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); - this.vorbisComments = null; + this.flacMetadata = null; } /** @@ -71,10 +72,13 @@ public final class FlacStreamMetadata { * @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 pictureList A list of pictures in the stream. * @see FLAC format * METADATA_BLOCK_STREAMINFO * @see FLAC format * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE */ public FlacStreamMetadata( int minBlockSize, @@ -85,7 +89,8 @@ public final class FlacStreamMetadata { int channels, int bitsPerSample, long totalSamples, - List vorbisComments) { + List vorbisComments, + List pictureList) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -94,7 +99,8 @@ public final class FlacStreamMetadata { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; - this.vorbisComments = parseVorbisComments(vorbisComments); + Metadata metadata = new Metadata(pictureList); + this.flacMetadata = metadata.copyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments)); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java new file mode 100644 index 0000000000..04a5b46e26 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java @@ -0,0 +1,43 @@ +/* + * 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 class PictureTest { + + @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/vorbis/VorbisCommentTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java similarity index 96% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java index 868b28b0e1..bb118e381a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.metadata.vorbis; +package com.google.android.exoplayer2.metadata.flac; import static com.google.common.truth.Truth.assertThat; 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 index 325d9b19f6..c556282ca2 100644 --- 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 @@ -19,7 +19,7 @@ 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.vorbis.VorbisComment; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,7 +34,9 @@ public final class FlacStreamMetadataTest { commentsList.add("Title=Song"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata.length()).isEqualTo(2); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -49,7 +51,9 @@ public final class FlacStreamMetadataTest { public void parseEmptyVorbisComments() { ArrayList commentsList = new ArrayList<>(); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata).isNull(); } @@ -59,7 +63,9 @@ public final class FlacStreamMetadataTest { ArrayList commentsList = new ArrayList<>(); commentsList.add("Title=So=ng"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -73,7 +79,9 @@ public final class FlacStreamMetadataTest { commentsList.add("TitleSong"); commentsList.add("Artist=Singer"); - Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments; + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) + .flacMetadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); 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 269c48c282..4ac007fa55 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 @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; 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; @@ -303,6 +304,7 @@ 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; public PlayerView(Context context) { this(context, null); @@ -1246,15 +1248,29 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private boolean setArtworkFromMetadata(Metadata metadata) { + boolean isArtworkSet = false; + int currentPicType = -1; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); + int picType; + byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + bitmapData = ((ApicFrame) metadataEntry).pictureData; + picType = ((ApicFrame) metadataEntry).pictureType; + } else if (metadataEntry instanceof PictureFrame) { + bitmapData = ((PictureFrame) metadataEntry).pictureData; + picType = ((PictureFrame) metadataEntry).pictureType; + } else { + continue; + } + /* Prefers the first front cover picture in the picture list */ + if (currentPicType != PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + currentPicType = picType; } } - return false; + return isArtworkSet; } private boolean setDrawableArtwork(@Nullable Drawable drawable) { From 846e0666dfe3de143668fcbfe932a14995893763 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 29 Jul 2019 11:06:24 +0100 Subject: [PATCH 241/807] Restructure updatePeriods code for better readability. Moved update of reading and playing periods in their own respective method. This is a no-op change. PiperOrigin-RevId: 260463668 --- .../exoplayer2/ExoPlayerImplInternal.java | 168 ++++++++++-------- .../android/exoplayer2/MediaPeriodQueue.java | 5 + 2 files changed, 101 insertions(+), 72 deletions(-) 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 738a30fad1..d356fe11d2 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 @@ -1490,82 +1490,88 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaSource.maybeThrowSourceInfoRefreshError(); return; } - - // Update the loading period if required. maybeUpdateLoadingPeriod(); + maybeUpdatePlayingPeriod(); + maybeUpdateReadingPeriod(); + } + + 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); + } + } 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. - return; - } - - // Advance the playing period if necessary. - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { 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. + while (shouldAdvancePlayingPeriod()) { 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(); + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); updatePlayingPeriodRenderers(oldPlayingPeriodHolder); playbackInfo = playbackInfo.copyWithNewPosition( - playingPeriodHolder.info.id, - playingPeriodHolder.info.startPositionUs, - playingPeriodHolder.info.contentPositionUs, + 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; } + } - 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(); + private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (readingPeriodHolder == null) { + return; + } + + 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(); @@ -1576,18 +1582,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; @@ -1615,23 +1621,41 @@ 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 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(); } } } 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 2597cd9b3f..9c0dd80a10 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 @@ -170,6 +170,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; } @@ -178,6 +179,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; } @@ -186,6 +188,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; } @@ -194,6 +197,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; } @@ -221,6 +225,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) { From 7703676c87a220277dbb89f80a53e164e01451cb Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 29 Jul 2019 11:06:50 +0100 Subject: [PATCH 242/807] Swap reading and playing media period updates. Both periods are rarely updated in the same iteration. If they are, advancing the reading period first seems more logical and also more efficient as it may avoid one extra doSomeWork iteration. PiperOrigin-RevId: 260463735 --- .../exoplayer2/ExoPlayerImplInternal.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) 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 d356fe11d2..b6317941fb 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 @@ -1491,8 +1491,8 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } maybeUpdateLoadingPeriod(); - maybeUpdatePlayingPeriod(); maybeUpdateReadingPeriod(); + maybeUpdatePlayingPeriod(); } private void maybeUpdateLoadingPeriod() throws IOException { @@ -1518,32 +1518,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - 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 void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { @@ -1621,6 +1595,32 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + 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; From d77d661e5283cb3ae55cbc2f29098e2f964e30b5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 13:41:48 +0100 Subject: [PATCH 243/807] Default viewport constraints to match primary display PiperOrigin-RevId: 260479923 --- RELEASENOTES.md | 2 + .../exoplayer2/castdemo/PlayerManager.java | 7 +- .../exoplayer2/gvrdemo/PlayerActivity.java | 3 +- .../exoplayer2/demo/DownloadTracker.java | 19 +-- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 2 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 2 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../android/exoplayer2/ExoPlayerFactory.java | 2 +- .../exoplayer2/offline/DownloadHelper.java | 121 +++++++++++++++--- .../trackselection/DefaultTrackSelector.java | 76 +++++++++-- .../offline/DownloadHelperTest.java | 10 +- .../DefaultTrackSelectorTest.java | 119 ++++++++--------- .../dash/offline/DownloadHelperTest.java | 4 +- .../hls/offline/DownloadHelperTest.java | 4 +- .../offline/DownloadHelperTest.java | 4 +- .../playbacktests/gts/DashTestRunner.java | 4 +- .../exoplayer2/testutil/ExoHostedTest.java | 3 +- .../testutil/ExoPlayerTestRunner.java | 2 +- 19 files changed, 259 insertions(+), 131 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 747436a69d..7afe0a78cd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Update `DefaultTrackSelector` to apply a viewport constraint for the default + display by default. * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). * Add basic DRM support to the Cast demo app. diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index b877ac7593..421269772c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -21,13 +21,11 @@ import androidx.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; @@ -40,7 +38,6 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource; 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.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; @@ -99,9 +96,7 @@ import org.json.JSONObject; currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); 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 index bd9c85da51..059f26b374 100644 --- 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 @@ -35,7 +35,6 @@ 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.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -135,7 +134,7 @@ public class PlayerActivity extends GvrPlayerActivity implements PlaybackPrepare DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this); - trackSelector = new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); + trackSelector = new DefaultTrackSelector(/* context= */ this); lastSeenTrackGroupArray = null; player = 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 40b1a94991..d8bfe23674 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 @@ -195,7 +195,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(); } } @@ -411,7 +411,7 @@ 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; 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/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/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/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..956f22f719 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)); } /** 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 4b5bf3c8a4..6952413129 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; @@ -82,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 { @@ -120,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); } @@ -133,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 @@ -158,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)); } /** @@ -197,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 @@ -208,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)); } /** @@ -247,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 @@ -258,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)); } /** @@ -370,10 +453,10 @@ 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(); 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 56eebfbee4..cc1742bb31 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 @@ -186,9 +186,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)); } /** @@ -656,8 +669,22 @@ 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 /** @@ -707,14 +734,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; /** @@ -1284,13 +1311,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") @@ -1298,10 +1328,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); } /** 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 479936b82f..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; @@ -113,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)); } @@ -244,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) @@ -281,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(); @@ -385,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(); 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 e450175524..4622dc1734 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); } @@ -234,7 +239,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 +276,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 +286,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 +301,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(); } @@ -426,8 +428,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}, @@ -452,8 +453,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}, @@ -523,7 +523,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}, @@ -605,8 +605,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}, @@ -648,8 +647,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}, @@ -983,10 +981,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)); @@ -997,15 +992,14 @@ 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 a language-matching track flagged as default should // be selected, and the one without forced flag should be preferred. - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("eng")); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly); @@ -1017,8 +1011,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); } @@ -1100,12 +1093,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); @@ -1114,7 +1107,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); @@ -1158,13 +1151,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); @@ -1190,7 +1183,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}, @@ -1221,7 +1214,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}, @@ -1269,7 +1262,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); @@ -1301,7 +1294,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); @@ -1335,7 +1328,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); @@ -1344,7 +1337,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); @@ -1353,7 +1346,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); @@ -1362,7 +1355,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)); @@ -1424,13 +1417,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); @@ -1456,7 +1449,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}, @@ -1468,7 +1461,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}, @@ -1503,7 +1496,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); @@ -1760,13 +1753,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; } @@ -1801,14 +1794,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/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 73225f68c7..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,6 +16,7 @@ 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; @@ -31,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]); @@ -39,6 +41,6 @@ public final class DownloadHelperTest { new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), - 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/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/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/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/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 b61c5f9b2c..9de7996d3c 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(); From 3051e5e9adcb059d9692d5f05ffd2e6377eeda70 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 29 Jul 2019 16:08:37 +0100 Subject: [PATCH 244/807] Ensure the SilenceMediaSource position is in range Issue: #6229 PiperOrigin-RevId: 260500986 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/source/SilenceMediaSource.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7afe0a78cd..5279a24698 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). * Flac extension: Parse `VORBIS_COMMENT` metadata ([#5527](https://github.com/google/ExoPlayer/issues/5527)). 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 index a5b78ef3f7..c3eab68983 100644 --- 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 @@ -117,6 +117,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @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]); @@ -143,6 +144,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public long seekToUs(long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < sampleStreams.size(); i++) { ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); } @@ -151,7 +153,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return positionUs; + return constrainSeekPosition(positionUs); } @Override @@ -171,6 +173,10 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public void reevaluateBuffer(long positionUs) {} + + private long constrainSeekPosition(long positionUs) { + return Util.constrainValue(positionUs, 0, durationUs); + } } private static final class SilenceSampleStream implements SampleStream { @@ -186,7 +192,7 @@ public final class SilenceMediaSource extends BaseMediaSource { } public void seekTo(long positionUs) { - positionBytes = getAudioByteCount(positionUs); + positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes); } @Override From 06f94815050b9f9e990c7c9d3d7b7eb0bbebecc1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 29 Jul 2019 17:41:04 +0100 Subject: [PATCH 245/807] Support different drm schemes in playlists in the demo app This CL changes PlayerActivity's VIEW_LIST action intent contract: Each media item configuration is provided by indexing the entries. For example, the URI of the first item is passed as "uri_0", the second one is "uri_1", etc. Optionally, the extra parameters, like the extensions, are passed as "extension_1", where the intent extras with matching indices, refer to the same media sample. The VIEW action's contract remains unchanged. PiperOrigin-RevId: 260518118 --- .../exoplayer2/demo/PlayerActivity.java | 275 ++++++++++-------- .../android/exoplayer2/demo/Sample.java | 187 ++++++++++++ .../demo/SampleChooserActivity.java | 148 ++-------- demos/main/src/main/res/values/strings.xml | 2 + 4 files changed, 360 insertions(+), 252 deletions(-) create mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java 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 d8bfe23674..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,6 +38,7 @@ 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; @@ -78,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"; @@ -124,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; @@ -132,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; @@ -148,6 +157,10 @@ public class PlayerActivity extends AppCompatActivity private AdsLoader adsLoader; private Uri loadedAdTagUri; + public PlayerActivity() { + mediaDrms = new ArrayList<>(); + } + // Activity lifecycle @Override @@ -329,69 +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; - } - DrmSessionManager 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; - } - } else { - drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + releaseMediaDrms(); + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { + return; } TrackSelection.Factory trackSelectionFactory; @@ -424,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], drmSessionManager); - } - 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; @@ -456,23 +391,113 @@ public class PlayerActivity extends AppCompatActivity updateButtonVisibility(); } - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource( - uri, - /* overrideExtension= */ null, - /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager()); + @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, - DrmSessionManager drmSessionManager) { + 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) @@ -508,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); } @@ -527,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() { @@ -588,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 @@ -718,5 +744,4 @@ public class PlayerActivity extends AppCompatActivity 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/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 From 961adb7e36f409c6a51e0d7f6ace017e3a23cd49 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 19:54:39 +0100 Subject: [PATCH 246/807] Cast: Add MediaItemConverter For now this just moves some code from the demo app to the extension. Eventually the goal would be to have CastPlayer playlist methods take MediaItem, have CastPlayer convert them internally to MediaQueueItem for sending to the Cast SDK, and also allow reverse conversion so we can reconstruct MediaItems from the Cast SDK's queue. PiperOrigin-RevId: 260548020 --- .../exoplayer2/castdemo/PlayerManager.java | 59 ++------------ .../ext/cast/DefaultMediaItemConverter.java | 81 +++++++++++++++++++ .../ext/cast/MediaItemConverter.java | 32 ++++++++ 3 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 421269772c..44d9a60ff2 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.net.Uri; -import androidx.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; @@ -30,7 +29,9 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; +import com.google.android.exoplayer2.ext.cast.MediaItemConverter; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -41,13 +42,9 @@ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; -import org.json.JSONException; -import org.json.JSONObject; /** Manages players and an internal media queue for the demo app. */ /* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { @@ -70,6 +67,7 @@ import org.json.JSONObject; private final ArrayList mediaQueue; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; + private final MediaItemConverter mediaItemConverter; private int currentItemIndex; private Player currentPlayer; @@ -95,6 +93,7 @@ import org.json.JSONObject; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); exoPlayer = ExoPlayerFactory.newSimpleInstance(context); exoPlayer.addListener(this); @@ -133,7 +132,7 @@ import org.json.JSONObject; mediaQueue.add(item); concatenatingMediaSource.addMediaSource(buildMediaSource(item)); if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); } } @@ -344,7 +343,7 @@ import org.json.JSONObject; if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) { MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; for (int i = 0; i < items.length; i++) { - items[i] = buildMediaQueueItem(mediaQueue.get(i)); + items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i)); } castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); } else { @@ -380,50 +379,4 @@ import org.json.JSONObject; throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } } - - private static MediaQueueItem buildMediaQueueItem(MediaItem item) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); - MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(item.uri.toString()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(item.mimeType) - .setMetadata(movieMetadata); - MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; - if (drmConfiguration != null) { - try { - // This configuration is only intended for testing and should *not* be used in production - // environments. See comment in the Cast Demo app's options provider. - JSONObject drmConfigurationJson = getDrmConfigurationJson(drmConfiguration); - if (drmConfigurationJson != null) { - mediaInfoBuilder.setCustomData(drmConfigurationJson); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build(); - } - - @Nullable - private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration) - throws JSONException { - 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 exoplayerConfig = - new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme); - if (drmConfiguration.licenseUri != null) { - exoplayerConfig.put("licenseUrl", drmConfiguration.licenseUri); - } - if (!drmConfiguration.requestHeaders.isEmpty()) { - exoplayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); - } - return new JSONObject().put("exoPlayerConfig", exoplayerConfig); - } } 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..c8db958d03 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java @@ -0,0 +1,81 @@ +/* + * 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.exoplayer2.C; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; +import org.json.JSONException; +import org.json.JSONObject; + +/** Default {@link MediaItemConverter} implementation. */ +public final class DefaultMediaItemConverter implements MediaItemConverter { + + @Override + public MediaQueueItem toMediaQueueItem(MediaItem item) { + if (item.mimeType == null) { + throw new IllegalArgumentException("The item must specify its mimeType"); + } + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + if (item.title != null) { + movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); + } + MediaInfo mediaInfo = + new MediaInfo.Builder(item.uri.toString()) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setContentType(item.mimeType) + .setMetadata(movieMetadata) + .setCustomData(getCustomData(item)) + .build(); + return new MediaQueueItem.Builder(mediaInfo).build(); + } + + private static JSONObject getCustomData(MediaItem item) { + JSONObject customData = new JSONObject(); + + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration == null) { + return customData; + } + + String drmScheme; + if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) { + drmScheme = "widevine"; + } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { + drmScheme = "playready"; + } else { + return customData; + } + + JSONObject exoPlayerConfig = new JSONObject(); + try { + exoPlayerConfig.put("withCredentials", false); + exoPlayerConfig.put("protectionSystem", drmScheme); + if (drmConfiguration.licenseUri != null) { + exoPlayerConfig.put("licenseUrl", drmConfiguration.licenseUri); + } + if (!drmConfiguration.requestHeaders.isEmpty()) { + exoPlayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); + } + customData.put("exoPlayerConfig", exoPlayerConfig); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return customData; + } +} 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..3cb2540ad8 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java @@ -0,0 +1,32 @@ +/* + * 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); + + // TODO: Add toMediaItem to convert in the opposite direction. +} From 46855884f583f95680e5928a0613ce494d08be58 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 29 Jul 2019 20:22:00 +0100 Subject: [PATCH 247/807] Fix samples' text in Cast demo app PiperOrigin-RevId: 260553467 --- .../android/exoplayer2/castdemo/MainActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 1a7f28cd77..244025f90d 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -281,5 +283,13 @@ public class MainActivity extends AppCompatActivity public SampleListAdapter(Context context) { super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setText(getItem(position).title); + return view; + } } } From 8be78d47ac5eeac8ae7b21abbe6e61c8f887f10e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 20:26:53 +0100 Subject: [PATCH 248/807] Cast: Add JSON serialization/deserialization for MediaItem This will allow the Cast extension to reconstruct MediaItems from MediaQueueItems obtained from the receiver's queue. PiperOrigin-RevId: 260554381 --- .../ext/cast/DefaultMediaItemConverter.java | 130 +++++++++++++++--- .../ext/cast/MediaItemConverter.java | 8 +- .../cast/DefaultMediaItemConverterTest.java | 66 +++++++++ 3 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java 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 index c8db958d03..098803a512 100644 --- 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 @@ -15,41 +15,132 @@ */ 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 movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); if (item.title != null) { - movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title); + metadata.putString(MediaMetadata.KEY_TITLE, item.title); } MediaInfo mediaInfo = new MediaInfo.Builder(item.uri.toString()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(item.mimeType) - .setMetadata(movieMetadata) + .setMetadata(metadata) .setCustomData(getCustomData(item)) .build(); return new MediaQueueItem.Builder(mediaInfo).build(); } - private static JSONObject getCustomData(MediaItem item) { - JSONObject customData = new JSONObject(); + // Deserialization. - MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + 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 customData; + return null; } String drmScheme; @@ -58,24 +149,19 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) { drmScheme = "playready"; } else { - return customData; + return null; } - JSONObject exoPlayerConfig = new JSONObject(); - try { - exoPlayerConfig.put("withCredentials", false); - exoPlayerConfig.put("protectionSystem", drmScheme); - if (drmConfiguration.licenseUri != null) { - exoPlayerConfig.put("licenseUrl", drmConfiguration.licenseUri); - } - if (!drmConfiguration.requestHeaders.isEmpty()) { - exoPlayerConfig.put("headers", new JSONObject(drmConfiguration.requestHeaders)); - } - customData.put("exoPlayerConfig", exoPlayerConfig); - } catch (JSONException e) { - throw new RuntimeException(e); + 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 customData; + return exoPlayerConfigJson; } } 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 index 3cb2540ad8..23633aa4d2 100644 --- 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 @@ -28,5 +28,11 @@ public interface MediaItemConverter { */ MediaQueueItem toMediaQueueItem(MediaItem mediaItem); - // TODO: Add toMediaItem to convert in the opposite direction. + /** + * 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/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); + } +} From 27a4f96cb17b727935f85e5a8c99e766b0711c72 Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Tue, 30 Jul 2019 11:47:33 +0530 Subject: [PATCH 249/807] Clean up FLAC picture parsing --- .../exoplayer2/ext/flac/FlacExtractor.java | 4 +- extensions/flac/src/main/jni/flac_jni.cc | 48 ++++++++----------- extensions/flac/src/main/jni/flac_parser.cc | 30 +++++++----- .../flac/src/main/jni/include/flac_parser.h | 14 +++--- .../metadata/flac/PictureFrame.java | 8 ++-- .../exoplayer2/util/FlacStreamMetadata.java | 12 ++--- ...PictureTest.java => PictureFrameTest.java} | 2 +- .../util/FlacStreamMetadataTest.java | 8 ++-- .../android/exoplayer2/ui/PlayerView.java | 21 +++++--- 9 files changed, 76 insertions(+), 71 deletions(-) rename library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/{PictureTest.java => PictureFrameTest.java} (97%) 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 9f79f09117..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 @@ -229,8 +229,8 @@ public final class FlacExtractor implements Extractor { binarySearchSeeker = outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamMetadata.flacMetadata != null) { - metadata = streamMetadata.flacMetadata.copyWithAppendedEntriesFrom(metadata); + if (streamMetadata.metadata != null) { + metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamMetadata, metadata, trackOutput); outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 4ccd24781b..9cc611559a 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -117,24 +117,24 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { } } - jobject pictureList = env->NewObject(arrayListClass, arrayListConstructor); - bool picValid = context->parser->isPicValid(); - if (picValid) { - std::vector pictures = context->parser->getPictures(); - jclass flacPictureFrameClass = env->FindClass( + jobject jPictures = 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 flacPictureFrameConstructor = env->GetMethodID( - flacPictureFrameClass, "", + jmethodID pictureFrameConstructor = env->GetMethodID( + pictureFrameClass, "", "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); - for (std::vector::const_iterator picture = pictures.begin(); + 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 picArr = env->NewByteArray(picture->data.size()); - env->SetByteArrayRegion(picArr, 0, picture->data.size(), + jbyteArray pictureData = env->NewByteArray(picture->data.size()); + env->SetByteArrayRegion(pictureData, 0, picture->data.size(), (signed char *)&picture->data[0]); - jobject flacPictureFrame = env->NewObject(flacPictureFrameClass, - flacPictureFrameConstructor, + jobject pictureFrame = env->NewObject(pictureFrameClass, + pictureFrameConstructor, picture->type, mimeType, description, @@ -142,11 +142,11 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { picture->height, picture->depth, picture->colors, - picArr); - env->CallBooleanMethod(pictureList, arrayListAddMethod, flacPictureFrame); + pictureData); + env->CallBooleanMethod(jPictures, arrayListAddMethod, pictureFrame); env->DeleteLocalRef(mimeType); env->DeleteLocalRef(description); - env->DeleteLocalRef(picArr); + env->DeleteLocalRef(pictureData); } } @@ -160,18 +160,12 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { flacStreamMetadataClass, "", "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); - jobject streamMetaData = 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, pictureList); - return streamMetaData; + 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, jPictures); } 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 fafc254482..b9e5cace71 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -193,18 +193,22 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { break; case FLAC__METADATA_TYPE_PICTURE: { - const FLAC__StreamMetadata_Picture *pic = &metadata->data.picture; - flacPicture picture; - picture.mimeType.assign(std::string(pic->mime_type)); - picture.description.assign(std::string((char *)pic->description)); - picture.data.assign(pic->data, pic->data + pic->data_length); - picture.width = pic->width; - picture.height = pic->height; - picture.depth = pic->depth; - picture.colors = pic->colors; - picture.type = pic->type; - mPictures.push_back(picture); - mPicValid = true; + const FLAC__StreamMetadata_Picture *parsedPicture = + &metadata->data.picture; + FlacPicture flacPicture; + flacPicture.mimeType.assign(std::string(parsedPicture->mime_type)); + flacPicture.description.assign( + std::string((char *)parsedPicture->description)); + flacPicture.data.assign( + parsedPicture->data, + parsedPicture->data + parsedPicture->data_length); + flacPicture.width = parsedPicture->width; + flacPicture.height = parsedPicture->height; + flacPicture.depth = parsedPicture->depth; + flacPicture.colors = parsedPicture->colors; + flacPicture.type = parsedPicture->type; + mPictures.push_back(flacPicture); + mPicturesValid = true; break; } default: @@ -269,7 +273,7 @@ FLACParser::FLACParser(DataSource *source) mEOF(false), mStreamInfoValid(false), mVorbisCommentsValid(false), - mPicValid(false), + mPicturesValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index f1d175b94f..9c6452c160 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -30,7 +30,7 @@ typedef int status_t; -typedef struct { +struct FlacPicture { int type; std::string mimeType; std::string description; @@ -39,7 +39,7 @@ typedef struct { FLAC__uint32 depth; FLAC__uint32 colors; std::vector data; -} flacPicture; +}; class FLACParser { public: @@ -65,9 +65,9 @@ class FLACParser { return mVorbisComments; } - bool isPicValid() const { return mPicValid; } + bool arePicturesValid() const { return mPicturesValid; } - const std::vector& getPictures() const { return mPictures; } + const std::vector& getPictures() const { return mPictures; } int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); @@ -97,7 +97,7 @@ class FLACParser { if (newPosition == 0) { mStreamInfoValid = false; mVorbisCommentsValid = false; - mPicValid = false; + mPicturesValid = false; mVorbisComments.clear(); mPictures.clear(); FLAC__stream_decoder_reset(mDecoder); @@ -150,8 +150,8 @@ class FLACParser { bool mVorbisCommentsValid; // cached when the PICTURE metadata is parsed by libFLAC - std::vector mPictures; - bool mPicValid; + std::vector mPictures; + bool mPicturesValid; // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; 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 index fcf1fd6e58..dc280be9ff 100644 --- 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 @@ -31,7 +31,7 @@ public final class PictureFrame implements Metadata.Entry { /** The mime type of the picture. */ public final String mimeType; /** A description of the picture. */ - @Nullable public final String description; + public final String description; /** The pixel width of the picture. */ public final int width; /** The pixel height of the picture. */ @@ -49,7 +49,7 @@ public final class PictureFrame implements Metadata.Entry { public PictureFrame( int pictureType, String mimeType, - @Nullable String description, + String description, int width, int height, int depth, @@ -111,8 +111,8 @@ public final class PictureFrame implements Metadata.Entry { public int hashCode() { int result = 17; result = 31 * result + pictureType; - result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); - result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + mimeType.hashCode(); + result = 31 * result + description.hashCode(); result = 31 * result + width; result = 31 * result + height; result = 31 * result + depth; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 48680b5095..e7851aa0a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -36,7 +36,7 @@ public final class FlacStreamMetadata { public final int channels; public final int bitsPerSample; public final long totalSamples; - @Nullable public final Metadata flacMetadata; + @Nullable public final Metadata metadata; private static final String SEPARATOR = "="; @@ -59,7 +59,7 @@ public final class FlacStreamMetadata { this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); - this.flacMetadata = null; + this.metadata = null; } /** @@ -72,7 +72,7 @@ public final class FlacStreamMetadata { * @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 pictureList A list of pictures in the stream. + * @param pictures A list of pictures in the stream. * @see FLAC format * METADATA_BLOCK_STREAMINFO * @see FLAC format @@ -90,7 +90,7 @@ public final class FlacStreamMetadata { int bitsPerSample, long totalSamples, List vorbisComments, - List pictureList) { + List pictures) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -99,8 +99,8 @@ public final class FlacStreamMetadata { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; - Metadata metadata = new Metadata(pictureList); - this.flacMetadata = metadata.copyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments)); + this.metadata = + new Metadata(pictures).copyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments)); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java similarity index 97% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java index 04a5b46e26..4263103eeb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java @@ -24,7 +24,7 @@ import org.junit.runner.RunWith; /** Test for {@link PictureFrame}. */ @RunWith(AndroidJUnit4.class) -public class PictureTest { +public final class PictureFrameTest { @Test public void testParcelable() { 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 index c556282ca2..3988e5e45e 100644 --- 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 @@ -36,7 +36,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata.length()).isEqualTo(2); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -53,7 +53,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata).isNull(); } @@ -65,7 +65,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); @@ -81,7 +81,7 @@ public final class FlacStreamMetadataTest { Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()) - .flacMetadata; + .metadata; assertThat(metadata.length()).isEqualTo(1); VorbisComment commentFrame = (VorbisComment) metadata.get(0); 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 4ac007fa55..1abdd33bb2 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 @@ -305,6 +305,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider 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); @@ -1249,25 +1250,31 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean setArtworkFromMetadata(Metadata metadata) { boolean isArtworkSet = false; - int currentPicType = -1; + int currentPictureType = PICTURE_TYPE_NOT_SET; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); - int picType; + int pictureType; byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { bitmapData = ((ApicFrame) metadataEntry).pictureData; - picType = ((ApicFrame) metadataEntry).pictureType; + pictureType = ((ApicFrame) metadataEntry).pictureType; } else if (metadataEntry instanceof PictureFrame) { bitmapData = ((PictureFrame) metadataEntry).pictureData; - picType = ((PictureFrame) metadataEntry).pictureType; + pictureType = ((PictureFrame) metadataEntry).pictureType; } else { continue; } - /* Prefers the first front cover picture in the picture list */ - if (currentPicType != PICTURE_TYPE_FRONT_COVER) { + /* Prefers the first front cover picture. + * If there are no front cover pictures, prefer the first picture in the list + * */ + if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); - currentPicType = picType; + currentPictureType = pictureType; + if (currentPictureType == PICTURE_TYPE_FRONT_COVER) { + /* Found a front cover, stop looking for more pictures. */ + break; + } } } return isArtworkSet; From 58006ac3adf61504b83f4ac879ab2c663d0037ec Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 29 Jul 2019 22:41:58 +0100 Subject: [PATCH 250/807] Tweak Firebase JobDispatcher extension README PiperOrigin-RevId: 260583198 --- extensions/jobdispatcher/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index bd76868625..a6f0c3966a 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,11 +1,11 @@ # ExoPlayer Firebase JobDispatcher extension # -**DEPRECATED** Please use [WorkManager extension][] or [`PlatformScheduler`]. +**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 +[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 ## @@ -24,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 - From 40926618ad887b72221e2301ab8e7118925cbcb1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 30 Jul 2019 11:21:52 +0100 Subject: [PATCH 251/807] Return the removed media source from ConcatenatingMediaSource.removeMediaSource PiperOrigin-RevId: 260681773 --- .../exoplayer2/source/ConcatenatingMediaSource.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 669a0e7bb4..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 @@ -263,9 +263,12 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource Date: Tue, 30 Jul 2019 11:32:40 +0100 Subject: [PATCH 252/807] Make blocking fixed track bandwidth the default and remove experimental flag. PiperOrigin-RevId: 260682878 --- .../AdaptiveTrackSelection.java | 76 +++++++------------ .../AdaptiveTrackSelectionTest.java | 3 + 2 files changed, 32 insertions(+), 47 deletions(-) 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 ca8a0b12f9..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 @@ -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. * @@ -666,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; } @@ -695,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); 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 91e7393fe7..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 @@ -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, From ce2e2797cb0a91496c27ee8650a90c064c05e935 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 30 Jul 2019 12:47:31 +0100 Subject: [PATCH 253/807] Improve DefaultMediaClock behaviour. DefaultMediaClock has currently two non-ideal behaviours: 1. One part of checking if it should use the renderer clock is checking whether the associated renderer finished reading its stream. This only makes sense if the renderer isn't already reading ahead into the next period. This can be solved by forwarding if we are reading ahead to the sync command. 2. When switching from stand-alone to renderer clock we assume they are exactly at the same position. This is true in theory, but in practise there may be small differences due to the different natures of these clocks. To prevent jumping backwards in time, we can temporarily stop the stand-alone clock and only switch once the renderer clock reached the same position. PiperOrigin-RevId: 260690468 --- .../android/exoplayer2/DefaultMediaClock.java | 88 ++++++++++++------- .../exoplayer2/ExoPlayerImplInternal.java | 4 +- .../exoplayer2/DefaultMediaClockTest.java | 79 +++++++++++------ 3 files changed, 111 insertions(+), 60 deletions(-) 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 410dffd558..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 @@ -40,11 +40,13 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); } - private final StandaloneMediaClock standaloneMediaClock; + private final StandaloneMediaClock standaloneClock; private final PlaybackParameterListener listener; @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,30 +118,25 @@ 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 @@ -146,32 +145,53 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; rendererClock.setPlaybackParameters(playbackParameters); playbackParameters = rendererClock.getPlaybackParameters(); } - standaloneMediaClock.setPlaybackParameters(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/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b6317941fb..488d002ab2 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 @@ -535,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; 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 c42edb32ae..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 @@ -53,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); } @@ -77,7 +78,7 @@ public class DefaultMediaClockTest { @Test public void standaloneStart_shouldStartClock() throws Exception { mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -98,7 +99,7 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.stop(); mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -130,7 +131,7 @@ 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 @@ -165,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); } @@ -174,6 +176,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @@ -183,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); } @@ -193,6 +198,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @@ -203,6 +209,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); verifyNoMoreInteractions(listener); } @@ -213,6 +220,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @@ -223,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(); } @@ -235,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 @@ -246,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); } @@ -283,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 @@ -293,7 +316,7 @@ public class DefaultMediaClockTest { /* isEnded= */ true, /* hasReadStreamToEnd= */ true); mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -302,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 @@ -312,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 @@ -328,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") From 78350cd17dd2dd03441267a2cbe3df0fe6a614c5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Jul 2019 16:14:06 +0100 Subject: [PATCH 254/807] Update javadoc for TrackOutput#sampleData to make it more clear that implementors aren't expected to rewind with setPosition() PiperOrigin-RevId: 260718614 --- .../com/google/android/exoplayer2/extractor/TrackOutput.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 6f2e24915d98a869f9ea360a1aa967bf8fcfed4b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 31 Jul 2019 11:16:48 +0100 Subject: [PATCH 255/807] Add @NonNullApi and annotate two packages with it. This new annotation declares everything as non-null by default and can be used as a package annotation in package-info.java. In this change the core lib offline package and the mediasession extension is annotated that way as initial example usage. PiperOrigin-RevId: 260894548 --- constants.gradle | 1 + .../ext/mediasession/package-info.java | 19 ++++++++++ library/core/build.gradle | 3 ++ .../exoplayer2/offline/package-info.java | 19 ++++++++++ .../android/exoplayer2/util/NonNullApi.java | 36 +++++++++++++++++++ 5 files changed, 78 insertions(+) create mode 100644 extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java diff --git a/constants.gradle b/constants.gradle index aba52817bc..b1c2c636c7 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,6 +24,7 @@ project.ext { autoValueVersion = '1.6' autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' + jsr305Version = '3.0.2' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' 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/library/core/build.gradle b/library/core/build.gradle index f532ae0e6a..ecb81c4450 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -59,8 +59,11 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.2' + 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/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/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 {} From af8f67c0686ecd6c12562aa55f0b4dd8edc8751b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 13:27:57 +0100 Subject: [PATCH 256/807] Don't print warning when skipping RIFF and FMT chunks They're not unexpected! PiperOrigin-RevId: 260907687 --- .../exoplayer2/extractor/wav/WavHeaderReader.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 7a6a7e346f..d76d3f37ea 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 @@ -121,13 +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); - final int data = 0x64617461; - while (chunkHeader.id != 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. - final int riff = 0x52494646; - if (chunkHeader.id == riff) { + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { From 80ab74748d3e8ee0a2c393830a2cc7593704af0a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 16:44:19 +0100 Subject: [PATCH 257/807] Mp3Extractor: Avoid outputting seek frame as a sample This could previously occur when seeking back to position=0 PiperOrigin-RevId: 260933636 --- .../android/exoplayer2/extractor/mp3/Mp3Extractor.java | 5 +++++ 1 file changed, 5 insertions(+) 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 8f13cfaa11..a448934359 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 @@ -116,6 +116,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; + private int firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -213,6 +214,10 @@ public final class Mp3Extractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = (int) input.getPosition(); + } else if (input.getPosition() == 0 && firstSamplePosition != 0) { + // Skip past the seek frame. + input.skipFully(firstSamplePosition); } return readSample(input); } From 288aa52decf6afacf90964f2d3a425c152677fea Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 18:02:21 +0100 Subject: [PATCH 258/807] Clean up some Ogg comments & document granulePosition PiperOrigin-RevId: 260947018 --- .../extractor/ogg/DefaultOggSeeker.java | 28 +++++++++---------- .../extractor/ogg/OggPageHeader.java | 14 +++++++--- 2 files changed, 24 insertions(+), 18 deletions(-) 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..9700760c49 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 @@ -147,12 +147,12 @@ import java.io.IOException; * which it is sensible to just skip pages to the target granule and pre-roll instead of doing * another seek request. * - * @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 + * @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. + * @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) @@ -263,8 +263,8 @@ 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) @@ -321,14 +321,14 @@ import java.io.IOException; * 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 + * @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. + * @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. */ @VisibleForTesting long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) 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 ff32ae3462..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 @@ -37,7 +37,13 @@ import java.io.IOException; 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; @@ -71,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. */ From 526cc72e0490760f723d50695d7a46fcf11059e5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 Jul 2019 19:54:12 +0100 Subject: [PATCH 259/807] WavExtractor: Skip to data start position if position reset to 0 PiperOrigin-RevId: 260970865 --- .../extractor/wav/WavExtractor.java | 2 ++ .../exoplayer2/extractor/wav/WavHeader.java | 28 +++++++++++++------ .../extractor/wav/WavHeaderReader.java | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) 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..d3114f9b69 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,6 +87,8 @@ 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(); 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..c7858dcd96 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 @@ -37,9 +37,9 @@ import com.google.android.exoplayer2.util.Util; @C.PcmEncoding private final int encoding; - /** Offset to the start of sample data. */ - private long dataStartPosition; - /** Total size in bytes of the sample data. */ + /** Position of the start of the sample data, in bytes. */ + private int dataStartPosition; + /** Total size of the sample data, in bytes. */ private long dataSize; public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.util.Util; this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +58,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 dataSize The total size of the sample data, in bytes. */ - public void setDataBounds(long dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataSize) { this.dataStartPosition = dataStartPosition; this.dataSize = dataSize; } - /** Returns the data limit, or {@link C#POSITION_UNSET} if the data bounds have not been set. */ + /** + * 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 limit of the sample data, in bytes, or {@link C#POSITION_UNSET} if the data bounds + * have not been set. + */ public long getDataLimit() { return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; } /** 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. 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 d76d3f37ea..839a9e3d5c 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 @@ -139,7 +139,7 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + wavHeader.setDataBounds((int) input.getPosition(), chunkHeader.size); } private WavHeaderReader() { From 561949a2251b6fc8c96f36771328c0a5e43fe4e2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 1 Aug 2019 09:45:57 +0100 Subject: [PATCH 260/807] Remove AnalyticsCollector.Factory. This factory was only needed in the past when we didn't have AnalyticsCollector.setPlayer. Code becomes easier to use without this factory. PiperOrigin-RevId: 261081860 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ExoPlayerFactory.java | 28 +++++++++---------- .../android/exoplayer2/SimpleExoPlayer.java | 19 +++++++------ .../analytics/AnalyticsCollector.java | 27 ++---------------- .../testutil/ExoPlayerTestRunner.java | 2 +- 5 files changed, 29 insertions(+), 49 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5279a24698..5bb858a2eb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ management in playlists. * Improve text selection logic to always prefer the better language matches over other selection parameters. +* Remove `AnalyticsCollector.Factory`. Instances can be created directly and + the `Player` set later using `AnalyticsCollector.setPlayer`. ### 2.10.4 ### 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 956f22f719..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 @@ -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/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index a782255cb8..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); 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 091696f8bf..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(); 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 9de7996d3c..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 @@ -622,7 +622,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc loadControl, /* drmSessionManager= */ null, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(clock), clock, Looper.myLooper()); } From a2cf427b4b2f6c4922d5f6c108f830e7460cdafd Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 10:34:11 +0100 Subject: [PATCH 261/807] Mp3Extractor: Avoid outputting non-zero position seek frame as a sample Checking inputPosition == 0 isn't sufficient because the synchronization at the top of read() may advance the input (i.e. in the case where there's some garbage prior to the seek frame). PiperOrigin-RevId: 261086901 --- .../exoplayer2/extractor/mp3/Mp3Extractor.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 a448934359..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 @@ -116,7 +116,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; - private int firstSamplePosition; + private long firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -214,10 +214,13 @@ public final class Mp3Extractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); - firstSamplePosition = (int) input.getPosition(); - } else if (input.getPosition() == 0 && firstSamplePosition != 0) { - // Skip past the seek frame. - input.skipFully(firstSamplePosition); + 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); } From b2c71e8b3f5162f74e33295009b3dd4a10d51f4b Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Thu, 1 Aug 2019 10:55:50 +0100 Subject: [PATCH 262/807] Extract VpxInputBuffer to a common class This class will be shared by both vp9 and av1 extension. PiperOrigin-RevId: 261089225 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 5 +++-- .../android/exoplayer2/ext/vp9/VpxDecoder.java | 17 ++++++++--------- .../video/VideoDecoderInputBuffer.java | 13 +++++-------- 3 files changed, 16 insertions(+), 19 deletions(-) rename extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java => library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java (70%) 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 56f5fd2d09..34301742e5 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; @@ -545,7 +546,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. } 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 0e13e82630..544259ffc0 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 @@ -22,13 +22,12 @@ 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 { +/** Vpx decoder. */ +/* package */ final class VpxDecoder + extends SimpleDecoder { public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_YUV = 0; @@ -65,7 +64,7 @@ import java.nio.ByteBuffer; 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."); } @@ -96,8 +95,8 @@ import java.nio.ByteBuffer; } @Override - protected VpxInputBuffer createInputBuffer() { - return new VpxInputBuffer(); + protected VideoDecoderInputBuffer createInputBuffer() { + return new VideoDecoderInputBuffer(); } @Override @@ -123,7 +122,7 @@ import java.nio.ByteBuffer; @Override @Nullable protected VpxDecoderException decode( - VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { + VideoDecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; 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); } From 1bb0703f2b61f0093d80c65211cd51ba30fd6f03 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 1 Aug 2019 12:15:59 +0100 Subject: [PATCH 263/807] return lg specific mime type as codec supported type for OMX.lge.alac.decoder ISSUE: #5938 PiperOrigin-RevId: 261097045 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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 e8fead61ae..46bc448a4a 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 @@ -370,6 +370,13 @@ public final class MediaCodecUtil { boolean secureDecodersExplicit, String requestedMimeType) { if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(requestedMimeType)) { + return supportedType; + } + } + if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { // Handle decoders that declare support for DV via MIME types that aren't // video/dolby-vision. @@ -379,13 +386,12 @@ public final class MediaCodecUtil { || "OMX.realtek.video.decoder.tunneled".equals(name)) { return "video/dv_hevc"; } - } - - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } + } else if (requestedMimeType.equals(MimeTypes.AUDIO_ALAC) + && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (requestedMimeType.equals(MimeTypes.AUDIO_FLAC) + && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; } } return null; From cbc1385fd32b55e44768df5416d323936541ff07 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 13:05:07 +0100 Subject: [PATCH 264/807] Some no-op cleanup for DefaultOggSeeker PiperOrigin-RevId: 261102008 --- .../extractor/ogg/DefaultOggSeeker.java | 110 ++++++++---------- 1 file changed, 49 insertions(+), 61 deletions(-) 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 9700760c49..a4aa6b8dd5 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 @@ -21,6 +21,7 @@ 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; @@ -206,39 +207,32 @@ import java.io.IOException; return -(pageHeader.granulePosition + 2); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; + /** + * 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 If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException 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); } - 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(); + return currentGranule; } /** @@ -266,8 +260,7 @@ import java.io.IOException; * @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); byte[] buffer = new byte[2048]; @@ -317,32 +310,27 @@ import java.io.IOException; 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 If populating the page header fails. - * @throws IOException If reading from the input fails. - * @throws InterruptedException 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 = + startPosition + + (targetGranule * (endPosition - startPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } } From 95ed5ce65d278d9793f409535a3035e8977cb6e9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 13:06:45 +0100 Subject: [PATCH 265/807] Make OggSeeker.startSeek take a granule rather than a time PiperOrigin-RevId: 261102180 --- .../exoplayer2/extractor/ogg/DefaultOggSeeker.java | 5 ++--- .../android/exoplayer2/extractor/ogg/FlacReader.java | 6 ++---- .../android/exoplayer2/extractor/ogg/OggSeeker.java | 10 ++++------ .../android/exoplayer2/extractor/ogg/StreamReader.java | 9 +++++---- 4 files changed, 13 insertions(+), 17 deletions(-) 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 a4aa6b8dd5..308547e510 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 @@ -120,12 +120,11 @@ import java.io.IOException; } @Override - public long startSeek(long timeUs) { + public void startSeek(long targetGranule) { Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); + this.targetGranule = targetGranule; state = STATE_SEEK; resetSeeking(); - return targetGranule; } @Override 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 d4c2bbb485..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 @@ -185,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 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/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index e459ad1e58..35a07fcf49 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; } } @@ -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 From cb8983afd10af000443119fcf3b4f0dcc7637749 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 16:14:12 +0100 Subject: [PATCH 266/807] Standardize ALAC initialization data Android considers ALAC initialization data to consider of the magic cookie only, where-as FFmpeg requires a full atom. Standardize around the Android definition, since it makes more sense (the magic cookie being contained within an atom is container specific, where-as the decoder shouldn't care what container the media stream is carried in) Issue: #5938 PiperOrigin-RevId: 261124155 --- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 47 ++++++++++++++----- .../exoplayer2/extractor/mp4/AtomParsers.java | 6 +-- 2 files changed, 35 insertions(+), 18 deletions(-) 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 12c26ca2ec..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 @@ -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/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 ea45374f86..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 @@ -1154,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. @@ -1166,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); From 79b86de619c5b43fc70680e85518b79db87b1153 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 1 Aug 2019 16:22:17 +0100 Subject: [PATCH 267/807] Use per-media source DRM in the Cast demo app PiperOrigin-RevId: 261125341 --- .../exoplayer2/castdemo/MainActivity.java | 16 +++ .../exoplayer2/castdemo/PlayerManager.java | 116 ++++++++++++++++-- demos/cast/src/main/res/values/strings.xml | 4 + 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 244025f90d..d0e40990be 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -35,6 +35,7 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; @@ -164,8 +165,23 @@ public class MainActivity extends AppCompatActivity } } + @Override + public void onUnsupportedTrack(int trackType) { + if (trackType == C.TRACK_TYPE_AUDIO) { + showToast(R.string.error_unsupported_audio); + } else if (trackType == C.TRACK_TYPE_VIDEO) { + showToast(R.string.error_unsupported_video); + } else { + // Do nothing. + } + } + // Internal methods. + private void showToast(int messageId) { + Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show(); + } + private View buildSampleListView() { View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); ListView sampleList = dialogList.findViewById(R.id.sample_list); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 44d9a60ff2..8b75eb0c74 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -28,6 +28,12 @@ import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; +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; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; @@ -36,15 +42,21 @@ import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; 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; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.Map; /** Manages players and an internal media queue for the demo app. */ /* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { @@ -54,6 +66,13 @@ import java.util.ArrayList; /** Called when the currently played item of the media queue changes. */ void onQueuePositionChanged(int previousIndex, int newIndex); + + /** + * Called when a track of type {@code trackType} is not supported by the player. + * + * @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants. + */ + void onUnsupportedTrack(int trackType); } private static final String USER_AGENT = "ExoCastDemoPlayer"; @@ -62,13 +81,16 @@ import java.util.ArrayList; private final PlayerView localPlayerView; private final PlayerControlView castControlView; + private final DefaultTrackSelector trackSelector; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; private final ArrayList mediaQueue; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; private final MediaItemConverter mediaItemConverter; + private final IdentityHashMap mediaDrms; + private TrackGroupArray lastSeenTrackGroupArray; private int currentItemIndex; private Player currentPlayer; @@ -94,8 +116,10 @@ import java.util.ArrayList; currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); mediaItemConverter = new DefaultMediaItemConverter(); + mediaDrms = new IdentityHashMap<>(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context); + trackSelector = new DefaultTrackSelector(context); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); @@ -162,7 +186,8 @@ import java.util.ArrayList; if (itemIndex == -1) { return false; } - concatenatingMediaSource.removeMediaSource(itemIndex); + MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex); + releaseMediaDrmOfMediaSource(removedMediaSource); if (currentPlayer == castPlayer) { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); @@ -238,6 +263,9 @@ import java.util.ArrayList; currentItemIndex = C.INDEX_UNSET; mediaQueue.clear(); concatenatingMediaSource.clear(); + for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) { + mediaDrm.release(); + } castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); localPlayerView.setPlayer(null); @@ -261,6 +289,25 @@ import java.util.ArrayList; updateCurrentItemIndex(); } + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); + } + if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) + == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); + } + } + lastSeenTrackGroupArray = trackGroups; + } + } + // CastPlayer.SessionAvailabilityListener implementation. @Override @@ -360,23 +407,78 @@ import java.util.ArrayList; } } - private static MediaSource buildMediaSource(MediaItem item) { + private MediaSource buildMediaSource(MediaItem item) { Uri uri = item.uri; String mimeType = item.mimeType; if (mimeType == null) { throw new IllegalArgumentException("mimeType is required"); } + + FrameworkMediaDrm mediaDrm = null; + DrmSessionManager drmSessionManager = + DrmSessionManager.getDummyDrmSessionManager(); + MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + if (drmConfiguration != null) { + String licenseServerUrl = + drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY); + for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { + drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); + } + try { + mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid); + drmSessionManager = + new DefaultDrmSessionManager<>( + drmConfiguration.uuid, + mediaDrm, + drmCallback, + /* optionalKeyRequestParameters= */ null, + /* multiSession= */ true); + } catch (UnsupportedDrmException e) { + // Do nothing. The track selector will avoid selecting the DRM protected tracks. + } + } + + MediaSource createdMediaSource; switch (mimeType) { case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new SsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new DashMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new HlsMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + createdMediaSource = + new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + break; default: throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } + if (mediaDrm != null) { + mediaDrms.put(createdMediaSource, mediaDrm); + } + return createdMediaSource; + } + + private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) { + FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource); + if (mediaDrmToRelease != null) { + mediaDrmToRelease.release(); + } } } diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 2f0acd4808..69f0691630 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -24,4 +24,8 @@ Failed to get Cast context. Try updating Google Play Services and restart the app. + Media includes video tracks, but none are playable by this device + + Media includes audio tracks, but none are playable by this device + From 4c40878b6b8d6662fd12e4068ed885e7332e2701 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 16:32:29 +0100 Subject: [PATCH 268/807] Shorten data length if it exceeds length of input Issue: #6241 PiperOrigin-RevId: 261126968 --- RELEASENOTES.md | 2 ++ .../extractor/wav/WavExtractor.java | 6 ++-- .../exoplayer2/extractor/wav/WavHeader.java | 36 +++++++++++-------- .../extractor/wav/WavHeaderReader.java | 13 +++++-- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c37556c98c..3c1748ea9d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. +* Calculate correct duration for clipped WAV streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). ### 2.10.4 ### 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 d3114f9b69..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 @@ -91,10 +91,10 @@ public final class WavExtractor implements Extractor { 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 c7858dcd96..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,17 +33,21 @@ 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; /** Position of the start of the sample data, in bytes. */ private int dataStartPosition; - /** Total size of the sample data, in bytes. */ - private long dataSize; + /** 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; @@ -51,6 +55,7 @@ import com.google.android.exoplayer2.util.Util; this.bitsPerSample = bitsPerSample; this.encoding = encoding; dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -59,11 +64,11 @@ 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 position of the start of the sample data, in bytes. - * @param dataSize The total size of the sample data, in bytes. + * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. */ - public void setDataBounds(int dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataEndPosition) { this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; + this.dataEndPosition = dataEndPosition; } /** @@ -75,11 +80,11 @@ import com.google.android.exoplayer2.util.Util; } /** - * Returns the limit of the sample data, in bytes, or {@link C#POSITION_UNSET} if the data bounds - * have not been set. + * 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 getDataLimit() { - return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; + public long getDataEndPosition() { + return dataEndPosition; } /** Returns whether the data start position and size have been set. */ @@ -96,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 839a9e3d5c..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 @@ -91,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); } /** @@ -139,7 +139,14 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds((int) 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() { From 42d3ca273ba94c85169f8a6697a8e9282a4a5737 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 1 Aug 2019 18:39:20 +0100 Subject: [PATCH 269/807] Propagate non-standard MIME type aliases Issue: #5938 PiperOrigin-RevId: 261150349 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 2 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 41 +++--- .../exoplayer2/mediacodec/MediaCodecUtil.java | 118 ++++++++++-------- .../video/MediaCodecVideoRenderer.java | 6 +- 5 files changed, 92 insertions(+), 77 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c1748ea9d..9dd79d8c42 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ the `Player` set later using `AnalyticsCollector.setPlayer`. * Calculate correct duration for clipped WAV streams ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). ### 2.10.4 ### 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 b965f4ef68..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 @@ -392,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); 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 acaf798b41..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); 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 46bc448a4a..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 @@ -171,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); @@ -268,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; @@ -289,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; } @@ -319,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)); @@ -338,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; } } @@ -352,48 +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)) { - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } - } + String mimeType) { + if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { + return null; + } - 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"; - } - } else if (requestedMimeType.equals(MimeTypes.AUDIO_ALAC) - && "OMX.lge.alac.decoder".equals(name)) { - return "audio/x-lg-alac"; - } else if (requestedMimeType.equals(MimeTypes.AUDIO_FLAC) - && "OMX.lge.flac.decoder".equals(name)) { - return "audio/x-lg-flac"; + 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; } @@ -403,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; } @@ -497,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; } @@ -522,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( 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 24524d057d..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 @@ -603,10 +603,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format format, MediaCrypto crypto, float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( format, + codecMimeType, codecMaxValues, codecOperatingRate, deviceNeedsNoPostProcessWorkaround, @@ -1164,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. @@ -1176,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); From 5eab519925c0d125074f770eae722121700ef2c5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 1 Aug 2019 19:36:18 +0100 Subject: [PATCH 270/807] Revert to using header bitrate for CBR MP3s A previous change switched to calculation of the bitrate based on the first MPEG audio header in the stream. This had the effect of fixing seeking to be consistent with playing from the start for streams where every frame has the same padding value, but broke streams where the encoder (correctly) modifies the padding value to match the declared bitrate in the header. Issue: #6238 PiperOrigin-RevId: 261163904 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/extractor/MpegAudioHeader.java | 4 ---- library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump | 2 +- library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9dd79d8c42..89acdfb9a4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ ([#6241](https://github.com/google/ExoPlayer/issues/6241)). * Fix Flac and ALAC playback on some LG devices ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* MP3: use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). ### 2.10.4 ### 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/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: From 0887ab059c1e78257ebc45ec86c9807c05094698 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 2 Aug 2019 09:28:17 +0100 Subject: [PATCH 271/807] Remove nullablity of track groups and selections in MediaPeriodHolder. These values can easily default to the empty track group and the empty selection. As a result we can remove restrictions about not calling holder.getTrackGroups before the period finished preparation. PiperOrigin-RevId: 261280927 --- .../exoplayer2/ExoPlayerImplInternal.java | 20 ++++---- .../android/exoplayer2/MediaPeriodHolder.java | 48 ++++++++----------- .../android/exoplayer2/MediaPeriodQueue.java | 9 +++- .../trackselection/TrackSelectorResult.java | 4 +- .../exoplayer2/MediaPeriodQueueTest.java | 10 +++- 5 files changed, 49 insertions(+), 42 deletions(-) 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 488d002ab2..4fe8da92c2 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 @@ -1108,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; } @@ -1197,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(); @@ -1506,7 +1503,12 @@ import java.util.concurrent.atomic.AtomicBoolean; } else { MediaPeriod mediaPeriod = queue.enqueueNextMediaPeriod( - rendererCapabilities, trackSelector, loadControl.getAllocator(), mediaSource, info); + rendererCapabilities, + trackSelector, + loadControl.getAllocator(), + mediaSource, + info, + emptyTrackSelectorResult); mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 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 a21afc4b51..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,8 +59,8 @@ 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; /** @@ -72,6 +72,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @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, @@ -79,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; 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 = @@ -167,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); @@ -202,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); @@ -303,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); } @@ -331,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++) { @@ -362,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++) { @@ -394,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/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 9c0dd80a10..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; @@ -135,13 +136,16 @@ 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.id.isAd() && info.contentPositionUs != C.TIME_UNSET @@ -155,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); 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/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index def7f8552e..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; @@ -381,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() { From 39317048e975042b34dde82283018f41d4880274 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Fri, 2 Aug 2019 10:27:35 +0100 Subject: [PATCH 272/807] Add video decoder exception class This will be used in common video renderer and decoder classes. PiperOrigin-RevId: 261287124 --- .../ext/vp9/VpxDecoderException.java | 4 +- .../video/VideoDecoderException.java | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java 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/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); + } +} From 4482db40e1b96e5ead3258cec90a738883ee1f5e Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Fri, 2 Aug 2019 11:44:48 +0100 Subject: [PATCH 273/807] Move output modes to constants file PiperOrigin-RevId: 261295173 --- .../ext/vp9/LibvpxVideoRenderer.java | 23 ++++++++++--------- .../exoplayer2/ext/vp9/VpxDecoder.java | 13 ++++------- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 10 ++++---- .../java/com/google/android/exoplayer2/C.java | 15 ++++++++++++ 4 files changed, 37 insertions(+), 24 deletions(-) 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 34301742e5..b6663ac3d7 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 @@ -137,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; @@ -275,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; } @@ -349,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; @@ -628,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); @@ -713,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); } @@ -735,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(); @@ -915,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 544259ffc0..93a4a2fc1f 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 @@ -29,10 +29,6 @@ import java.nio.ByteBuffer; /* 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; - private static final int NO_ERROR = 0; private static final int DECODE_ERROR = 1; private static final int DRM_ERROR = 2; @@ -40,7 +36,7 @@ import java.nio.ByteBuffer; private final ExoMediaCrypto exoMediaCrypto; private final long vpxDecContext; - private volatile int outputMode; + @C.VideoOutputMode private volatile int outputMode; /** * Creates a VP9 decoder. @@ -87,10 +83,9 @@ 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; } @@ -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); 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 30d7b8e92c..de411089ab 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,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.OutputBuffer; import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; @@ -31,7 +32,8 @@ public final class VpxOutputBuffer extends OutputBuffer { /** Decoder private data. */ public int decoderPrivate; - public int mode; + /** Output mode. */ + @C.VideoOutputMode public int mode; /** * RGB buffer for RGB mode. */ @@ -60,10 +62,10 @@ public final class VpxOutputBuffer extends OutputBuffer { * 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}, {@link - * VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}. + * @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, int mode) { + public void init(long timeUs, @C.VideoOutputMode int mode) { this.timeUs = timeUs; this.mode = mode; } 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 8ded5038b0..9ed5cb7e36 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 @@ -499,6 +499,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}. From 851218aca931c855247acef48eaa791daa3aae7c Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 2 Aug 2019 14:20:15 +0100 Subject: [PATCH 274/807] Fix AnalyticsCollectorTest flakiness. Two tests have very low propability flakiness (1:1000) due to not waiting for a seek in one case and the chance of already being ended in another case. Fix these and also adjust wrong comments about state changes. PiperOrigin-RevId: 261309976 --- .../exoplayer2/analytics/AnalyticsCollectorTest.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 a2546adfe4..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 @@ -368,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); @@ -378,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 */, @@ -505,6 +504,7 @@ public final class 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(); @@ -522,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); @@ -585,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 */, From 90ab05c574dd2f9b2ffb635fc3c3c4128040191d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 14:42:14 +0100 Subject: [PATCH 275/807] Add DRM samples to Cast demo app PiperOrigin-RevId: 261312509 --- .../android/exoplayer2/castdemo/DemoUtil.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 91ea0c92e2..dacdbfe616 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -36,7 +36,6 @@ import java.util.List; public static final List SAMPLES; static { - // App samples. ArrayList samples = new ArrayList<>(); // Clear content. @@ -59,6 +58,45 @@ import java.util.List; .setMimeType(MIME_TYPE_VIDEO_MP4) .build()); + // DRM content. + samples.add( + new MediaItem.Builder() + .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd")) + .setTitle("Widevine DASH cenc: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")) + .setTitle("Widevine DASH cbc1: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + samples.add( + new MediaItem.Builder() + .setUri( + Uri.parse( + "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")) + .setTitle("Widevine DASH cbcs: Tears") + .setMimeType(MIME_TYPE_DASH) + .setDrmConfiguration( + new DrmConfiguration( + C.WIDEVINE_UUID, + Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"), + Collections.emptyMap())) + .build()); + SAMPLES = Collections.unmodifiableList(samples); } From 91c62ea26f99f04c67fde77cf7380f21d0729e54 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:20:35 +0100 Subject: [PATCH 276/807] Fix DefaultOggSeeker seeking - When in STATE_SEEK with targetGranule==0, seeking would exit without checking that the input was positioned at the correct place. - Seeking could fail due to trying to read beyond the end of the stream. - Seeking was not robust against IO errors during the skip phase that occurs after the binary search has sufficiently converged. PiperOrigin-RevId: 261317035 --- .../extractor/ogg/DefaultOggSeeker.java | 177 ++++++++---------- .../extractor/ogg/StreamReader.java | 2 +- .../extractor/ogg/DefaultOggSeekerTest.java | 107 +++++------ .../ogg/DefaultOggSeekerUtilMethodsTest.java | 95 +--------- 4 files changed, 129 insertions(+), 252 deletions(-) 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 308547e510..064bd5732d 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,6 +16,7 @@ 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; @@ -35,11 +36,12 @@ import java.io.IOException; 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; @@ -55,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 { @@ -91,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; } @@ -101,137 +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 void startSeek(long targetGranule) { - Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - this.targetGranule = targetGranule; - state = STATE_SEEK; - resetSeeking(); - } - @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 = targetGranule; + 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. + * @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); } /** - * 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. + * Skips forward to the start of the page containing the {@code targetGranule}. * * @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 If populating the page header fails. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) + private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException, InterruptedException { - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ 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); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); } input.resetPeekPosition(); - return currentGranule; } /** @@ -244,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(); } @@ -261,7 +237,7 @@ import java.io.IOException; */ 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) { @@ -302,8 +278,8 @@ 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; @@ -320,10 +296,11 @@ import java.io.IOException; public SeekPoints getSeekPoints(long timeUs) { long targetGranule = streamReader.convertTimeToGranule(timeUs); long estimatedPosition = - startPosition - + (targetGranule * (endPosition - startPosition) / totalGranules) + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) - DEFAULT_OFFSET; - estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1); + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); } 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 35a07fcf49..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 @@ -148,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); 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..fba358ea51 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,7 +16,6 @@ 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; @@ -36,9 +35,9 @@ public final class DefaultOggSeekerTest { 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); @@ -62,9 +61,9 @@ public final class DefaultOggSeekerTest { 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, /* firstPayloadPageIsLastPage= */ false); @@ -78,70 +77,56 @@ 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 last granule. + granule = seekTo(input, oggSeeker, testFile.lastGranule, 0); + long position = testFile.data.length; + // TODO: Simplify this. + assertThat( + (testFile.lastGranule > granule && position > input.getPosition()) + || (testFile.lastGranule == granule && 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 exact granule. + input.setPosition(testFile.data.length / 2); + oggSeeker.skipToNextPage(input); + assertThat(pageHeader.populate(input, true)).isTrue(); + position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; + granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); + // TODO: Simplify this. + assertThat( + (pageHeader.granulePosition > granule && position > input.getPosition()) + || (pageHeader.granulePosition == granule && position == input.getPosition())) + .isTrue(); for (int i = 0; i < 100; i += 1) { long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); 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); - - 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."); - } + pageHeader.populate(input, false); + // The target granule should be within the current page. + assertThat(granule).isAtMost(targetGranule); + assertThat(targetGranule).isLessThan(pageHeader.granulePosition); } } @@ -149,18 +134,15 @@ public final class DefaultOggSeekerTest { 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 +153,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 index d6691f50f8..2521602228 100644 --- 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 @@ -85,9 +85,9 @@ public final class DefaultOggSeekerUtilMethodsTest { throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ extractorInput.getLength(), /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 2, /* firstPayloadPageIsLastPage= */ false); @@ -99,87 +99,6 @@ public final class DefaultOggSeekerUtilMethodsTest { } } - @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( @@ -204,7 +123,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertReadGranuleOfLastPage(input, 60000); fail(); } catch (EOFException e) { - // ignored + // Ignored. } } @@ -216,7 +135,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertReadGranuleOfLastPage(input, 60000); fail(); } catch (IllegalArgumentException e) { - // ignored + // Ignored. } } @@ -224,9 +143,9 @@ public final class DefaultOggSeekerUtilMethodsTest { throws IOException, InterruptedException { DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 2, /* firstPayloadPageIsLastPage= */ false); @@ -235,7 +154,7 @@ public final class DefaultOggSeekerUtilMethodsTest { assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); break; } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored + // Ignored. } } } From 5e98d76e8b5ca816006400fb2e84e44b4d3a90c4 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:29:12 +0100 Subject: [PATCH 277/807] Improve extractor tests based on ExtractorAsserts - Test seeking to (timeUs=0, position=0), which should always work and produce the same output as initially reading from the start of the stream. - Reset the input when testing seeking, to ensure IO errors are simulated for this case. PiperOrigin-RevId: 261317898 --- .../exoplayer2/testutil/ExtractorAsserts.java | 17 +++++++++++++---- .../exoplayer2/testutil/FakeExtractorInput.java | 9 +++++++++ .../testutil/FakeExtractorOutput.java | 6 ++++++ 3 files changed, 28 insertions(+), 4 deletions(-) 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/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index 20f6f436b0..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,6 +80,15 @@ 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. * 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); From 173eadc70ed0d63eeab7935d4d3ee3c58ea89fc1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 15:48:44 +0100 Subject: [PATCH 278/807] Move DefaultOggSeeker tests into a single class PiperOrigin-RevId: 261320318 --- .../extractor/ogg/DefaultOggSeekerTest.java | 137 ++++++++++++++- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 162 ------------------ 2 files changed, 136 insertions(+), 163 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java 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 fba358ea51..fd649f0924 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 @@ -20,8 +20,12 @@ 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; @@ -31,6 +35,8 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { @@ -55,6 +61,95 @@ 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(); @@ -130,7 +225,47 @@ public final class DefaultOggSeekerTest { } } - private long seekTo( + 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 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; 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 2521602228..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ /dev/null @@ -1,162 +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( - /* 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 */ } - } - } - - @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( - /* 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. - } - } - } - -} From f179feb292fca717cf7a1af02b8c31a308810ed2 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 16:49:17 +0100 Subject: [PATCH 279/807] Constraint seek targetGranule within bounds + simplify tests PiperOrigin-RevId: 261328701 --- .../extractor/ogg/DefaultOggSeeker.java | 8 +-- .../extractor/ogg/DefaultOggSeekerTest.java | 26 ++------- .../exoplayer2/extractor/ogg/OggTestFile.java | 58 +++++++++++-------- 3 files changed, 43 insertions(+), 49 deletions(-) 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 064bd5732d..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 @@ -29,8 +29,8 @@ 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; @@ -127,7 +127,7 @@ import java.io.IOException; @Override public void startSeek(long targetGranule) { - this.targetGranule = targetGranule; + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); state = STATE_SEEK; start = payloadStartPosition; end = payloadEndPosition; @@ -201,7 +201,7 @@ import java.io.IOException; private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException, InterruptedException { pageHeader.populate(input, /* quiet= */ false); - while (pageHeader.granulePosition < targetGranule) { + while (pageHeader.granulePosition <= targetGranule) { input.skipFully(pageHeader.headerSize + pageHeader.bodySize); start = input.getPosition(); startGranule = pageHeader.granulePosition; 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 fd649f0924..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 @@ -160,7 +160,7 @@ public final class DefaultOggSeekerTest { /* payloadStartPosition= */ 0, /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, - /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, + /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount, /* firstPayloadPageIsLastPage= */ false); OggPageHeader pageHeader = new OggPageHeader(); @@ -183,28 +183,12 @@ public final class DefaultOggSeekerTest { assertThat(input.getPosition()).isEqualTo(0); // Test last granule. - granule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - // TODO: Simplify this. - assertThat( - (testFile.lastGranule > granule && position > input.getPosition()) - || (testFile.lastGranule == granule && position == input.getPosition())) - .isTrue(); - - // Test exact granule. - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - // TODO: Simplify this. - assertThat( - (pageHeader.granulePosition > granule && position > input.getPosition()) - || (pageHeader.granulePosition == granule && position == input.getPosition())) - .isTrue(); + 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); granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); 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) { From 818ef62fb0626f878089a5c50edfe0da49e32a32 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 18:02:21 +0100 Subject: [PATCH 280/807] Remove obsolete workaround PiperOrigin-RevId: 261340526 --- build.gradle | 8 -------- 1 file changed, 8 deletions(-) diff --git a/build.gradle b/build.gradle index bc538ead68..1d0b459bf5 100644 --- a/build.gradle +++ b/build.gradle @@ -21,14 +21,6 @@ buildscript { classpath 'com.novoda:bintray-release:0.9' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } - // Workaround for the following test coverage issue. Remove when fixed: - // https://code.google.com/p/android/issues/detail?id=226070 - configurations.all { - resolutionStrategy { - force 'org.jacoco:org.jacoco.report:0.7.4.201502262128' - force 'org.jacoco:org.jacoco.core:0.7.4.201502262128' - } - } } allprojects { repositories { From 6f014749b3e96d573a081837e92a66805b992de1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 18:05:17 +0100 Subject: [PATCH 281/807] Upgrade dependency versions PiperOrigin-RevId: 261341256 --- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/workmanager/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index e067789bc4..83e994c5e1 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.2.0' + api 'com.google.android.gms:play-services-cast-framework:17.0.0' implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 0808ad6c44..7561857dca 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ 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' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 9065855a3f..ea7564316f 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.0.1' + implementation 'androidx.work:work-runtime:2.1.0' } ext { From fb0481c520b5b7eefa51abe01625a16b5009013d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 2 Aug 2019 19:03:03 +0100 Subject: [PATCH 282/807] Bump annotations dependency + update release notes PiperOrigin-RevId: 261353271 --- RELEASENOTES.md | 20 ++++++++++---------- demos/gvr/build.gradle | 2 +- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 2 +- extensions/gvr/build.gradle | 2 +- extensions/ima/build.gradle | 2 +- extensions/leanback/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- extensions/opus/build.gradle | 2 +- extensions/rtmp/build.gradle | 2 +- extensions/vp9/build.gradle | 2 +- library/core/build.gradle | 2 +- library/dash/build.gradle | 2 +- library/hls/build.gradle | 2 +- library/smoothstreaming/build.gradle | 2 +- library/ui/build.gradle | 2 +- playbacktests/build.gradle | 2 +- testutils/build.gradle | 2 +- testutils_robolectric/build.gradle | 2 +- 23 files changed, 32 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 89acdfb9a4..bfdf9e733e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,12 +27,6 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. -* Calculate correct duration for clipped WAV streams - ([#6241](https://github.com/google/ExoPlayer/issues/6241)). -* Fix Flac and ALAC playback on some LG devices - ([#5938](https://github.com/google/ExoPlayer/issues/5938)). -* MP3: use CBR header bitrate, not calculated bitrate. This reverts a change - from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). ### 2.10.4 ### @@ -41,6 +35,14 @@ ExoPlayer library classes. * Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* WAV: Calculate correct duration for clipped streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). * Fix issue where initial seek positions get ignored when playing a preroll ad ([#6201](https://github.com/google/ExoPlayer/issues/6201)). * Fix issue where invalid language tags were normalized to "und" instead of @@ -48,10 +50,8 @@ ([#6153](https://github.com/google/ExoPlayer/issues/6153)). * Fix `DataSchemeDataSource` re-opening and range requests ([#6192](https://github.com/google/ExoPlayer/issues/6192)). -* Ensure the `SilenceMediaSource` position is in range - ([#6229](https://github.com/google/ExoPlayer/issues/6229)). -* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata - ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). ### 2.10.3 ### diff --git a/demos/gvr/build.gradle b/demos/gvr/build.gradle index 457af80a8d..37d8fbbb99 100644 --- a/demos/gvr/build.gradle +++ b/demos/gvr/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-gvr') - 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/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/extensions/cast/build.gradle b/extensions/cast/build.gradle index 83e994c5e1..68a7494a3f 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:17.0.0' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 7561857dca..f7cc707fb4 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -33,7 +33,7 @@ android { dependencies { 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') diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index ffecdcd16f..15952b1860 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,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 testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 10b244cb39..c67de27697 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,7 +39,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 androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion 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/ima/build.gradle b/extensions/ima/build.gradle index 2df9448d08..0ef9f281c9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,7 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } 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/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/opus/build.gradle b/extensions/opus/build.gradle index 0795079c6b..28f7b05465 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index ca734c3657..b74be659ee 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index fe1f220af6..51b2677368 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index ecb81c4450..93126d9830 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -58,7 +58,7 @@ 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 diff --git a/library/dash/build.gradle b/library/dash/build.gradle index f6981a2220..9f5775d478 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8e9696af70..82e09ab72c 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index a2e81fb304..fa67ea1d01 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 6384bf920f..5182dfccf5 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ 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') } 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/testutils/build.gradle b/testutils/build.gradle index 36465f5d5f..afd2a146af 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -41,7 +41,7 @@ dependencies { api 'org.mockito:mockito-core:' + mockitoVersion 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 diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index 78fa5dbd87..a098178429 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -41,6 +41,6 @@ dependencies { api 'org.robolectric:robolectric:' + robolectricVersion api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion } From d6e74bc19b05a3c4cc1d6c7684a4692aea5ce1e3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 10:21:55 +0100 Subject: [PATCH 283/807] Ensure position reset keep window sequence number. We currently keep the sequence number if we don't reset the position. However, the sequence number should be kept if we don't reset the state. Otherwise re-prepare with position reset is counted as new playback although it's still the same. PiperOrigin-RevId: 261644924 --- .../android/exoplayer2/ExoPlayerImpl.java | 2 +- .../exoplayer2/ExoPlayerImplInternal.java | 6 +-- .../android/exoplayer2/PlaybackInfo.java | 19 ++++++--- .../android/exoplayer2/ExoPlayerTest.java | 42 +++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) 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 3eed66402d..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 @@ -647,7 +647,7 @@ 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; 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 4fe8da92c2..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 @@ -646,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; @@ -884,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); @@ -896,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; 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 669f41ca13..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 @@ -150,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); } /** 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 d5b0b2c667..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 @@ -28,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; @@ -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; @@ -1434,6 +1436,46 @@ 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); From 790deb71db4a344fe65e06e8f17e9c31744c13a7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 10:43:50 +0100 Subject: [PATCH 284/807] Check if controller is used when performing click directly. Issue:#6260 PiperOrigin-RevId: 261647858 --- RELEASENOTES.md | 3 +++ .../java/com/google/android/exoplayer2/ui/PlayerView.java | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfdf9e733e..03b45fc945 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,9 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.4 ### 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 d97a53bc4d..ec6e94e042 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 @@ -1156,6 +1156,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1491,9 +1494,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onSingleTapUp(MotionEvent e) { - if (!useController || player == null) { - return false; - } return toggleControllerVisibility(); } } From c9b73cba8c06cb3928c077bdb47bfa4873b92684 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 5 Aug 2019 11:13:07 +0100 Subject: [PATCH 285/807] Update release notes PiperOrigin-RevId: 261651655 --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 03b45fc945..4ffd8c4e5d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,9 +27,6 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. -* Fix issue when calling `performClick` on `PlayerView` without - `PlayerControlView` - ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.4 ### @@ -55,6 +52,9 @@ ([#6192](https://github.com/google/ExoPlayer/issues/6192)). * Fix Flac and ALAC playback on some LG devices ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). ### 2.10.3 ### From b6441a02f5eb8a8377e90db1b392ae2ad9b372e1 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Mon, 5 Aug 2019 16:40:20 +0100 Subject: [PATCH 286/807] Introduce common output buffer class for video decoders PiperOrigin-RevId: 261693054 --- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 113 +--------------- .../video/VideoDecoderOutputBuffer.java | 125 ++++++++++++++++++ 2 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java 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 de411089ab..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,39 +15,12 @@ */ package com.google.android.exoplayer2.ext.vp9; -import com.google.android.exoplayer2.C; -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; - - /** 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; public VpxOutputBuffer(VpxDecoder owner) { this.owner = owner; @@ -58,84 +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 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; - } - 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; - } - - /** - * 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; - } - - 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/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); + } +} From 17a9030e1d02220efbf32e8c65ca925f630e66ad Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 16:57:44 +0100 Subject: [PATCH 287/807] Update stale TrackSelections in chunk sources when keeping the streams. If we keep streams in chunk sources after selecting new tracks, we also keep a reference to a stale disabled TrackSelection object. Fix this by updating the TrackSelection object when keeping the stream. The static part of the selection (i.e. the subset of selected tracks) stays the same in all cases. Issue:#6256 PiperOrigin-RevId: 261696082 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/source/MediaPeriod.java | 5 ++++- .../exoplayer2/source/dash/DashChunkSource.java | 7 +++++++ .../exoplayer2/source/dash/DashMediaPeriod.java | 16 +++++++++++++--- .../source/dash/DefaultDashChunkSource.java | 7 ++++++- .../exoplayer2/source/hls/HlsChunkSource.java | 10 ++++------ .../source/hls/HlsSampleStreamWrapper.java | 17 ++++++++++------- .../smoothstreaming/DefaultSsChunkSource.java | 7 ++++++- .../source/smoothstreaming/SsChunkSource.java | 7 +++++++ .../source/smoothstreaming/SsMediaPeriod.java | 1 + 10 files changed, 61 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4ffd8c4e5d..06cfde8d6c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -55,6 +55,9 @@ * Fix issue when calling `performClick` on `PlayerView` without `PlayerControlView` ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). ### 2.10.3 ### 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 f076eae32c..847c87b077 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 @@ -106,13 +106,16 @@ 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 received {@link TrackSelection TrackSelections} are no longer valid and + * references need to be replaced even if the corresponding {@link SampleStream} is kept. + * *

          This method is only called after the period has been prepared. * * @param selections The renderer track selections. 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 b34b677d45..5daa1a8fd5 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 @@ -406,17 +406,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 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 2de81a2535..bcf0a1766a 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 @@ -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) { 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..ee5a5f0809 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 @@ -183,17 +183,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/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index c8c1b8f566..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 @@ -306,14 +306,17 @@ import java.util.Set; 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) { 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 286ec82ed6..d103358d37 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 @@ -131,6 +131,7 @@ import java.util.List; stream.release(); streams[i] = null; } else { + stream.getChunkSource().updateTrackSelection(selections[i]); sampleStreamsList.add(stream); } } From 346f8e670af19b33e4575fa200f90008c7360cba Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Aug 2019 17:21:31 +0100 Subject: [PATCH 288/807] Turn on non-null-by-default for most extensions. PiperOrigin-RevId: 261700729 --- .../exoplayer2/ext/cast/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/cronet/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/ffmpeg/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/flac/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../exoplayer2/ext/ima/package-info.java | 19 +++++++++++++++++++ .../ext/jobdispatcher/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/leanback/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/okhttp/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/opus/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/ext/rtmp/package-info.java | 19 +++++++++++++++++++ .../ext/workmanager/package-info.java | 19 +++++++++++++++++++ 12 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java create mode 100644 extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java create mode 100644 extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java create mode 100644 extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java create mode 100644 extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java create mode 100644 extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java create mode 100644 extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java create mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java create mode 100644 extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java create mode 100644 extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java 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/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/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/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/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 249271dc61..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 @@ -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, 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/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/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/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/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/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/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; From 591bd6e46a0c10138fe0ea466c42378c030fbc89 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 10:34:15 +0100 Subject: [PATCH 289/807] Fix UI module API nullability annotations and make non-null-by-default. PiperOrigin-RevId: 261872025 --- .../exoplayer2/ui/PlayerControlView.java | 10 ++++---- .../android/exoplayer2/ui/PlayerView.java | 23 ++++++++++++------- .../android/exoplayer2/ui/package-info.java | 19 +++++++++++++++ .../exoplayer2/ui/spherical/package-info.java | 19 +++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java 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 e408035e98..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 @@ -285,7 +285,7 @@ public class PlayerControlView extends FrameLayout { } public PlayerControlView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs, /* defStyleAttr= */ 0); } public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { @@ -494,9 +494,10 @@ 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; } @@ -512,7 +513,8 @@ public class PlayerControlView extends FrameLayout { /** * 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; 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 ec6e94e042..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 @@ -308,14 +308,14 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider 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()) { @@ -505,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; } @@ -904,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); } @@ -914,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); @@ -1006,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); } @@ -1025,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; } @@ -1047,6 +1053,7 @@ 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; } 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/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; From 4603188165e465cdec9d5e971c649cb398c50c43 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Aug 2019 11:16:02 +0100 Subject: [PATCH 290/807] Add inband emsg-v1 support to FragmentedMp4Extractor This also decouples EventMessageEncoder's serialization schema from the emesg spec (it happens to still match the emsg-v0 spec, but this is no longer required). PiperOrigin-RevId: 261877918 --- .../java/com/google/android/exoplayer2/C.java | 7 +- .../extractor/mp4/FragmentedMp4Extractor.java | 92 ++++++++++++------- .../metadata/emsg/EventMessageDecoder.java | 8 +- 3 files changed, 66 insertions(+), 41 deletions(-) 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 9ed5cb7e36..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; /** 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 392d4d9179..7ff7912729 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; @@ -141,6 +143,8 @@ public class FragmentedMp4Extractor implements Extractor { // Adjusts sample timestamps. @Nullable private final TimestampAdjuster timestampAdjuster; + private final EventMessageEncoder eventMessageEncoder; + // Parser state. private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; @@ -254,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); @@ -591,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); } @@ -631,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(); 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..87d0491a7b 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 @@ -25,13 +25,7 @@ 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"; From 3b9288b805e2bf26d4d4ce2c131075afd007f3ae Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Aug 2019 11:16:40 +0100 Subject: [PATCH 291/807] Migrate literal usages of 1000 to (new) C.MILLIS_PER_SECOND This only covers calls to scaleLargeTimestamp() PiperOrigin-RevId: 261878019 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 9 ++++++--- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 4 +++- .../source/dash/manifest/DashManifestParser.java | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) 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 7ff7912729..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 @@ -965,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; @@ -993,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/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 87d0491a7b..a49bf956b3 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -46,7 +47,8 @@ public final class EventMessageDecoder implements MetadataDecoder { // 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 = + Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); 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 8bf142f397..1419f8198c 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 @@ -899,7 +899,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); From b0330edc0b83105a034b95cb9dca096a2ed1e1c6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 12:46:39 +0100 Subject: [PATCH 292/807] Fix some Android Studio nullness warning created by new @NonNullApi. PiperOrigin-RevId: 261888086 --- .../mediasession/MediaSessionConnector.java | 28 ++++++++++--------- .../ext/mediasession/TimelineQueueEditor.java | 2 +- .../exoplayer2/offline/ActionFile.java | 6 ++-- .../offline/ActionFileUpgradeUtil.java | 2 +- .../exoplayer2/offline/DownloadHelper.java | 4 +-- .../exoplayer2/offline/DownloadManager.java | 16 +++++------ .../exoplayer2/offline/DownloadService.java | 8 +++--- .../exoplayer2/offline/SegmentDownloader.java | 2 +- 8 files changed, 35 insertions(+), 33 deletions(-) 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 be085ae30b..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 @@ -558,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; @@ -571,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; @@ -673,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); } /** @@ -684,7 +684,7 @@ public final class MediaSessionConnector { */ public final void invalidateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); - Player player = this.player; + @Nullable Player player = this.player; if (player == null) { builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); mediaSession.setPlaybackState(builder.build()); @@ -693,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); @@ -703,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; @@ -949,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) { @@ -968,37 +970,37 @@ public final class MediaSessionConnector { } } } - CharSequence title = description.getTitle(); + @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); } - CharSequence subtitle = description.getSubtitle(); + @Nullable CharSequence subtitle = description.getSubtitle(); if (subtitle != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle)); } - CharSequence displayDescription = description.getDescription(); + @Nullable CharSequence displayDescription = description.getDescription(); if (displayDescription != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, String.valueOf(displayDescription)); } - Bitmap iconBitmap = description.getIconBitmap(); + @Nullable Bitmap iconBitmap = description.getIconBitmap(); if (iconBitmap != null) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap); } - Uri iconUri = description.getIconUri(); + @Nullable Uri iconUri = description.getIconUri(); if (iconUri != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri)); } - String mediaId = description.getMediaId(); + @Nullable String mediaId = description.getMediaId(); if (mediaId != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId); } - Uri mediaUri = description.getMediaUri(); + @Nullable Uri mediaUri = description.getMediaUri(); if (mediaUri != null) { builder.putString( MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri)); 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/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 6952413129..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 @@ -390,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; @@ -808,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; } 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 107cedd728..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 @@ -592,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 |= @@ -611,7 +611,7 @@ public abstract class DownloadService extends Service { // 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 { @@ -644,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 { 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( From a9b93d7ec2b30450c1f15dbf1b1abefd0c8cb705 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 15:34:36 +0100 Subject: [PATCH 293/807] Fix some remaining extension API nullability issues. PiperOrigin-RevId: 261910303 --- .../exoplayer2/ext/cast/CastUtils.java | 3 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 6 ++-- .../ext/flac/LibflacAudioRenderer.java | 6 ++-- .../ext/opus/LibopusAudioRenderer.java | 6 ++-- .../exoplayer2/ext/opus/OpusDecoder.java | 31 ++++++++++++++----- .../audio/SimpleDecoderAudioRenderer.java | 9 +++--- .../audio/SimpleDecoderAudioRendererTest.java | 29 +++++++++-------- 7 files changed, 56 insertions(+), 34 deletions(-) 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/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/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 376d0fd75e..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 @@ -51,8 +51,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @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; @@ -66,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/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 b8b9598989..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 @@ -79,8 +79,8 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @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) @@ -99,7 +99,7 @@ public 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; 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 dbce33b923..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 @@ -44,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; @@ -66,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."); @@ -232,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/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 b17fa75181..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. @@ -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 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) From 73d6a0f2bdd48b8a335b485511bc088ad748e158 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Thu, 8 Aug 2019 09:17:55 +0200 Subject: [PATCH 294/807] Automatically show closed captioning/hearing impaired text track --- .../trackselection/DefaultTrackSelector.java | 17 +++++++++++- .../TrackSelectionParameters.java | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) 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 cc1742bb31..e20daeebad 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 @@ -432,6 +432,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + @Override + public ParametersBuilder setPreferredRoleFlags(int preferredRoleFlags) { + super.setPreferredRoleFlags(preferredRoleFlags); + return this; + } + @Override public ParametersBuilder setSelectUndeterminedTextLanguage( boolean selectUndeterminedTextLanguage) { @@ -642,6 +648,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedSampleRateAdaptiveness, // Text preferredTextLanguage, + preferredRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags, // General @@ -837,6 +844,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* allowAudioMixedSampleRateAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, + TrackSelectionParameters.DEFAULT.preferredRoleFlags, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, TrackSelectionParameters.DEFAULT.disabledTextTrackSelectionFlags, // General @@ -869,6 +877,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean allowAudioMixedSampleRateAdaptiveness, // Text @Nullable String preferredTextLanguage, + int preferredRoleFlags, boolean selectUndeterminedTextLanguage, @C.SelectionFlags int disabledTextTrackSelectionFlags, // General @@ -882,6 +891,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { super( preferredAudioLanguage, preferredTextLanguage, + preferredRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); // Video @@ -2590,6 +2600,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final boolean isDefault; private final boolean hasPreferredIsForcedFlag; private final int preferredLanguageScore; + private final int preferredRoleFlagsScore; private final int selectedAudioLanguageScore; public TextTrackScore( @@ -2606,6 +2617,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { preferredLanguageScore = getFormatLanguageScore( format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + preferredRoleFlagsScore = format.roleFlags & parameters.preferredRoleFlags; // 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. @@ -2616,7 +2628,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); isWithinConstraints = - preferredLanguageScore > 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); + (preferredLanguageScore > 0 || (parameters.preferredTextLanguage == null && selectedAudioLanguageScore > 0 && preferredRoleFlagsScore > 0)) || isDefault || (isForced && selectedAudioLanguageScore > 0); } /** @@ -2634,6 +2646,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.preferredLanguageScore != other.preferredLanguageScore) { return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); } + if (this.preferredRoleFlagsScore != other.preferredRoleFlagsScore) { + return compareInts(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore); + } if (this.isDefault != other.isDefault) { return this.isDefault ? 1 : -1; } 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 81af551b68..2edbbc8bd4 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 @@ -33,6 +33,7 @@ public class TrackSelectionParameters implements Parcelable { @Nullable /* package */ String preferredAudioLanguage; @Nullable /* package */ String preferredTextLanguage; + @C.RoleFlags /* package */ int preferredRoleFlags; /* package */ boolean selectUndeterminedTextLanguage; @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; @@ -48,6 +49,7 @@ public class TrackSelectionParameters implements Parcelable { /* package */ Builder(TrackSelectionParameters initialValues) { preferredAudioLanguage = initialValues.preferredAudioLanguage; preferredTextLanguage = initialValues.preferredTextLanguage; + preferredRoleFlags = initialValues.preferredRoleFlags; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; } @@ -74,6 +76,17 @@ public class TrackSelectionParameters implements Parcelable { return this; } + /** + * See {@link TrackSelectionParameters#preferredRoleFlags}. + * + * @param preferredRoleFlags Preferred role flags. + * @return This builder. + */ + public Builder setPreferredRoleFlags(int preferredRoleFlags) { + this.preferredRoleFlags = preferredRoleFlags; + return this; + } + /** * See {@link TrackSelectionParameters#selectUndeterminedTextLanguage}. * @@ -102,6 +115,7 @@ public class TrackSelectionParameters implements Parcelable { preferredAudioLanguage, // Text preferredTextLanguage, + preferredRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); } @@ -121,6 +135,11 @@ public class TrackSelectionParameters implements Parcelable { * the default track if there is one, or no track otherwise. The default value is {@code null}. */ @Nullable public final String preferredTextLanguage; + /** + * The preferred role flags for text tracks. {@code 0} selects + * the default track if there is one, or no track otherwise. The default value is {@code 0}. + */ + @Nullable public final int preferredRoleFlags; /** * Whether a text track with undetermined language should be selected if no track with {@link * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The @@ -138,6 +157,7 @@ public class TrackSelectionParameters implements Parcelable { /* preferredAudioLanguage= */ null, // Text /* preferredTextLanguage= */ null, + /* preferredRoleFlags= */ 0, /* selectUndeterminedTextLanguage= */ false, /* disabledTextTrackSelectionFlags= */ 0); } @@ -145,12 +165,14 @@ public class TrackSelectionParameters implements Parcelable { /* package */ TrackSelectionParameters( @Nullable String preferredAudioLanguage, @Nullable String preferredTextLanguage, + int preferredRoleFlags, boolean selectUndeterminedTextLanguage, @C.SelectionFlags int disabledTextTrackSelectionFlags) { // Audio this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); // Text this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); + this.preferredRoleFlags = preferredRoleFlags; this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; } @@ -158,6 +180,7 @@ public class TrackSelectionParameters implements Parcelable { /* package */ TrackSelectionParameters(Parcel in) { this.preferredAudioLanguage = in.readString(); this.preferredTextLanguage = in.readString(); + this.preferredRoleFlags = in.readInt(); this.selectUndeterminedTextLanguage = Util.readBoolean(in); this.disabledTextTrackSelectionFlags = in.readInt(); } @@ -179,6 +202,7 @@ public class TrackSelectionParameters implements Parcelable { TrackSelectionParameters other = (TrackSelectionParameters) obj; return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) + && preferredRoleFlags == other.preferredRoleFlags && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags; } @@ -188,6 +212,7 @@ public class TrackSelectionParameters implements Parcelable { int result = 1; result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); + result = 31 * result + (preferredRoleFlags); result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); result = 31 * result + disabledTextTrackSelectionFlags; return result; @@ -204,6 +229,7 @@ public class TrackSelectionParameters implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeString(preferredAudioLanguage); dest.writeString(preferredTextLanguage); + dest.writeInt(preferredRoleFlags); Util.writeBoolean(dest, selectUndeterminedTextLanguage); dest.writeInt(disabledTextTrackSelectionFlags); } From 831de75f89f2c3230323277d3899def6b1306788 Mon Sep 17 00:00:00 2001 From: Yannick RUI Date: Fri, 9 Aug 2019 10:42:12 +0200 Subject: [PATCH 295/807] Missing documentation link --- .../android/exoplayer2/trackselection/DefaultTrackSelector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e20daeebad..d07682f3b9 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 @@ -877,7 +877,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean allowAudioMixedSampleRateAdaptiveness, // Text @Nullable String preferredTextLanguage, - int preferredRoleFlags, + @C.RoleFlags int preferredRoleFlags, boolean selectUndeterminedTextLanguage, @C.SelectionFlags int disabledTextTrackSelectionFlags, // General From fd803a39a3470bb9b806d7cf76594e1192ee9d28 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Aug 2019 16:18:33 +0100 Subject: [PATCH 296/807] Further MediaPeriod.selectTracks documentation tweak PiperOrigin-RevId: 261917229 --- .../google/android/exoplayer2/source/MediaPeriod.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 847c87b077..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 @@ -113,15 +113,17 @@ public interface MediaPeriod extends SequenceableLoader { * 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 received {@link TrackSelection TrackSelections} are no longer valid and - * references need to be replaced even if the corresponding {@link SampleStream} is kept. + *

          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 From 6617862f0b3a4a829892a51f01fa914b86104122 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 6 Aug 2019 17:28:15 +0100 Subject: [PATCH 297/807] Add allowAudioMixedChannelCountAdaptiveness parameter to DefaultTrackSelector. We already allow mixed mime type and mixed sample rate adaptation on request, so for completeness, we can also allow mixed channel count adaptation. Issue:#6257 PiperOrigin-RevId: 261930046 --- RELEASENOTES.md | 4 ++ .../trackselection/DefaultTrackSelector.java | 55 ++++++++++++++++--- .../DefaultTrackSelectorTest.java | 1 + 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 06cfde8d6c..7c934c478c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,10 @@ over other selection parameters. * Remove `AnalyticsCollector.Factory`. Instances can be created directly and the `Player` set later using `AnalyticsCollector.setPlayer`. +* Add `allowAudioMixedChannelCountAdaptiveness` parameter to + `DefaultTrackSelector` to allow adaptive selections of audio tracks with + different channel counts + ([#6257](https://github.com/google/ExoPlayer/issues/6257)). ### 2.10.4 ### 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 cc1742bb31..77a7acc9e4 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 @@ -177,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; @@ -227,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; @@ -424,6 +427,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * See {@link Parameters#allowAudioMixedChannelCountAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( + boolean allowAudioMixedChannelCountAdaptiveness) { + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + return this; + } + // Text @Override @@ -640,6 +654,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary, allowAudioMixedMimeTypeAdaptiveness, allowAudioMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness, // Text preferredTextLanguage, selectUndeterminedTextLanguage, @@ -775,6 +790,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 /** @@ -835,6 +856,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* exceedAudioConstraintsIfNecessary= */ true, /* allowAudioMixedMimeTypeAdaptiveness= */ false, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, @@ -867,6 +889,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedAudioConstraintsIfNecessary, boolean allowAudioMixedMimeTypeAdaptiveness, boolean allowAudioMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness, // Text @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -901,6 +924,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; @@ -934,6 +958,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); @@ -1015,6 +1040,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 @@ -1045,6 +1072,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); @@ -1081,6 +1109,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); @@ -1989,7 +2018,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { formatSupports[selectedGroupIndex], params.maxAudioBitrate, params.allowAudioMixedMimeTypeAdaptiveness, - params.allowAudioMixedSampleRateAdaptiveness); + params.allowAudioMixedSampleRateAdaptiveness, + params.allowAudioMixedChannelCountAdaptiveness); if (adaptiveTracks.length > 0) { definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks); } @@ -2007,7 +2037,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int selectedConfigurationTrackCount = 0; AudioConfigurationTuple selectedConfiguration = null; HashSet seenConfigurationTuples = new HashSet<>(); @@ -2024,7 +2055,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness); + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness); if (configurationCount > selectedConfigurationTrackCount) { selectedConfiguration = configuration; selectedConfigurationTrackCount = configurationCount; @@ -2044,7 +2076,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { selectedConfiguration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { adaptiveIndices[index++] = i; } } @@ -2059,7 +2092,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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( @@ -2068,7 +2102,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { count++; } } @@ -2081,11 +2116,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { return isSupported(formatSupport, false) && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate) - && (format.channelCount != Format.NO_VALUE - && format.channelCount == configuration.channelCount) + && (allowAudioMixedChannelCountAdaptiveness + || (format.channelCount != Format.NO_VALUE + && format.channelCount == configuration.channelCount)) && (allowMixedMimeTypeAdaptiveness || (format.sampleMimeType != null && TextUtils.equals(format.sampleMimeType, configuration.mimeType))) 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 4622dc1734..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 @@ -148,6 +148,7 @@ public final class DefaultTrackSelectorTest { /* exceedAudioConstraintsIfNecessary= */ false, /* allowAudioMixedMimeTypeAdaptiveness= */ true, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ true, // Text /* preferredTextLanguage= */ "de", /* selectUndeterminedTextLanguage= */ true, From 113e25dc740490104c69afb454315ca210b0b23d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 11:36:41 +0100 Subject: [PATCH 298/807] Clean up documentation of DefaultTrackSelector.ParametersBuilder. We don't usually refer to other classes when documenting method parameters but rather duplicate the actual definition. PiperOrigin-RevId: 262102714 --- .../trackselection/DefaultTrackSelector.java | 131 +++++++++++++----- .../TrackSelectionParameters.java | 20 ++- 2 files changed, 109 insertions(+), 42 deletions(-) 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 77a7acc9e4..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 @@ -261,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) { @@ -272,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) { @@ -282,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) { @@ -292,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( @@ -303,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( @@ -314,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( @@ -329,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( @@ -350,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( @@ -375,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) { @@ -385,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) { @@ -395,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( @@ -406,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( @@ -417,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( @@ -428,8 +459,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedChannelCountAdaptiveness}. + * 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( @@ -462,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) { @@ -472,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) { @@ -499,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( @@ -510,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 @@ -520,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; @@ -534,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) { @@ -570,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) { @@ -591,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) { @@ -610,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); @@ -621,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. @@ -703,8 +760,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // 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 @@ -712,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 @@ -721,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; /** @@ -736,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; @@ -772,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; /** @@ -781,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; /** 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 81af551b68..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 @@ -53,9 +53,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * 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) { @@ -64,9 +65,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * 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) { @@ -75,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) { @@ -85,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( From 79e962c55a42844cde02710fec1e9644c9221cc5 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 7 Aug 2019 14:18:22 +0100 Subject: [PATCH 299/807] Expose a method on EventMessageDecoder that returns EventMessage directly PiperOrigin-RevId: 262121134 --- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 a49bf956b3..340b662e97 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 @@ -37,7 +37,10 @@ public final class EventMessageDecoder implements MetadataDecoder { 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(); @@ -50,8 +53,9 @@ public final class EventMessageDecoder implements MetadataDecoder { long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); 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); } } From 074b6f8ebd14371ccfa699fadacb8b15dcdc1b57 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 14:36:57 +0100 Subject: [PATCH 300/807] Fix DASH module API nullability issues and add package-level non-null-by-default PiperOrigin-RevId: 262123595 --- library/dash/build.gradle | 1 + .../source/dash/DashMediaPeriod.java | 9 +- .../source/dash/DashMediaSource.java | 30 +++--- .../exoplayer2/source/dash/DashUtil.java | 16 ++-- .../source/dash/DefaultDashChunkSource.java | 6 +- .../source/dash/manifest/DashManifest.java | 16 ++-- .../dash/manifest/DashManifestParser.java | 94 +++++++++++-------- .../source/dash/manifest/Descriptor.java | 12 +-- .../dash/manifest/ProgramInformation.java | 16 ++-- .../source/dash/manifest/RangedUri.java | 2 +- .../source/dash/manifest/Representation.java | 45 +++++---- .../source/dash/manifest/SegmentBase.java | 50 ++++++---- .../source/dash/manifest/package-info.java | 19 ++++ .../source/dash/offline/package-info.java | 19 ++++ .../exoplayer2/source/dash/package-info.java | 19 ++++ 15 files changed, 228 insertions(+), 126 deletions(-) create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 9f5775d478..c34ed8c907 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,6 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } 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 5daa1a8fd5..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 @@ -60,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 @@ -245,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); 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 576491b464..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 @@ -130,7 +130,7 @@ 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; @@ -430,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, @@ -455,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, @@ -492,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, @@ -529,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, @@ -569,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, @@ -591,10 +591,10 @@ 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, 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 bcf0a1766a..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) { @@ -633,7 +633,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { this( periodDurationUs, representation, @@ -787,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/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 1419f8198c..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; @@ -47,6 +48,7 @@ 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; @@ -189,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, @@ -259,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); @@ -394,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; @@ -477,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); @@ -564,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); @@ -650,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; @@ -675,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); @@ -711,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); @@ -756,15 +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, + @Nullable SegmentTemplate parent, List adaptationSetSupplementalProperties) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -819,8 +823,8 @@ public class DashManifestParser extends DefaultHandler long endNumber, long duration, List timeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { return new SegmentTemplate( initialization, timescale, @@ -1008,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); @@ -1126,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; } @@ -1159,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; } @@ -1230,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)) { @@ -1264,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) @@ -1273,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) { @@ -1485,14 +1494,19 @@ public class DashManifestParser extends DefaultHandler 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 9ac1257ee2..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,7 +83,7 @@ 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. */ @Nullable 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 ba4faafd95..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; /** @@ -308,9 +323,9 @@ public abstract class SegmentBase { long startNumber, long endNumber, long duration, - List segmentTimeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + @Nullable List segmentTimeline, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { super( initialization, timescale, @@ -324,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; From 58d4fd93dd07d9ad7af8b76eed111be5550bb0d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Aug 2019 14:42:47 +0100 Subject: [PATCH 301/807] Fix HLS module API nullability issues and add package-level non-null-by-default PiperOrigin-RevId: 262124441 --- library/hls/build.gradle | 1 + .../source/hls/Aes128DataSource.java | 3 ++- .../hls/DefaultHlsExtractorFactory.java | 6 +++--- .../exoplayer2/source/hls/HlsChunkSource.java | 10 ++++------ .../source/hls/HlsExtractorFactory.java | 7 ++++--- .../exoplayer2/source/hls/HlsMediaPeriod.java | 9 +++++++-- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/hls/WebvttExtractor.java | 5 +++-- .../source/hls/offline/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/source/hls/package-info.java | 19 +++++++++++++++++++ .../playlist/DefaultHlsPlaylistTracker.java | 4 +++- .../hls/playlist/HlsMasterPlaylist.java | 4 ++-- .../source/hls/playlist/HlsMediaPlaylist.java | 7 +++---- .../source/hls/playlist/package-info.java | 19 +++++++++++++++++++ 14 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 82e09ab72c..8301820e79 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -41,6 +41,7 @@ android { dependencies { 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') } 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 022d62cbfc..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 @@ -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 ee5a5f0809..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; 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 e21827557a..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 @@ -54,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. @@ -249,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]; 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 877b6d486e..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 @@ -110,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; 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 a4fd28009f..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 @@ -172,6 +172,7 @@ public final class DefaultHlsPlaylistTracker } @Override + @Nullable public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); if (snapshot != null && isForPlayback) { @@ -448,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; @@ -467,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; From 79d627d441bc2f49cc1f7832a959085907953c70 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 7 Aug 2019 15:56:25 +0100 Subject: [PATCH 302/807] Simplify EventMessageEncoder/Decoder serialization We're no longer tied to the emsg spec, so we can skip unused fields and assume ms for duration. Also remove @Nullable annotation from EventMessageEncoder#encode, it seems the current implementation never returns null PiperOrigin-RevId: 262135009 --- .../metadata/emsg/EventMessageDecoder.java | 15 +--- .../metadata/emsg/EventMessageEncoder.java | 4 -- .../emsg/EventMessageDecoderTest.java | 19 ++--- .../emsg/EventMessageEncoderTest.java | 69 ++++++++----------- 4 files changed, 40 insertions(+), 67 deletions(-) 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 340b662e97..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 @@ -15,22 +15,17 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import com.google.android.exoplayer2.C; 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 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) { @@ -43,15 +38,7 @@ public final class EventMessageDecoder implements MetadataDecoder { 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(), C.MILLIS_PER_SECOND, timescale); + long durationMs = emsgData.readUnsignedInt(); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); 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/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); } From 70b912c23e445ce89aa8932a88ad7d149fcd52d2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 09:22:26 +0100 Subject: [PATCH 303/807] Fix API nullability of remaining extensions and mark them as non-null-by-default PiperOrigin-RevId: 262303610 --- .../ext/vp9/LibvpxVideoRenderer.java | 16 +++++++-------- .../exoplayer2/ext/vp9/VpxDecoder.java | 20 ++++++++++++++----- .../ext/vp9/VpxVideoSurfaceView.java | 5 +++-- .../exoplayer2/ext/vp9/package-info.java | 19 ++++++++++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java 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 b6663ac3d7..5e9d8d0897 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 @@ -174,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, @@ -206,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( @@ -249,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, 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 93a4a2fc1f..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 @@ -33,7 +33,7 @@ import java.nio.ByteBuffer; 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; @C.VideoOutputMode private volatile int outputMode; @@ -55,7 +55,7 @@ import java.nio.ByteBuffer; int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - ExoMediaCrypto exoMediaCrypto, + @Nullable ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads) @@ -170,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/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; From 313bd109517351e758f5c0a0085b9d780a459e6e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 09:44:58 +0100 Subject: [PATCH 304/807] Fix SS module API nullability issues and add package-level non-null-by-default PiperOrigin-RevId: 262306255 --- library/smoothstreaming/build.gradle | 1 + .../source/smoothstreaming/SsMediaPeriod.java | 9 +++- .../source/smoothstreaming/SsMediaSource.java | 30 ++++++------ .../smoothstreaming/manifest/SsManifest.java | 46 ++++++++++++++----- .../manifest/SsManifestParser.java | 21 +++++---- .../manifest/package-info.java | 19 ++++++++ .../smoothstreaming/offline/package-info.java | 19 ++++++++ .../source/smoothstreaming/package-info.java | 19 ++++++++ 8 files changed, 127 insertions(+), 37 deletions(-) create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index fa67ea1d01..d85ecbb1a3 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,6 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } 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 d103358d37..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 @@ -38,6 +38,7 @@ 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 @@ -120,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) { 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 3c0593200e..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 @@ -115,7 +115,7 @@ 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; @@ -370,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, @@ -395,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, @@ -431,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, @@ -466,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); } @@ -496,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, @@ -515,10 +515,10 @@ 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, 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 39e22f2982..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); 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; From bbe681a904d58fe1f84f6c7c6e3390d932c86249 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 11:09:33 +0100 Subject: [PATCH 305/807] Make Kotlin JVM annotations available and use in ExoPlayer. NoExternal PiperOrigin-RevId: 262316962 --- constants.gradle | 1 + library/core/build.gradle | 3 +-- library/core/proguard-rules.txt | 3 ++- .../com/google/android/exoplayer2/util/NonNullApi.java | 8 +++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/constants.gradle b/constants.gradle index b1c2c636c7..9510b8442e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,6 +25,7 @@ project.ext { autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.31' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/library/core/build.gradle b/library/core/build.gradle index 93126d9830..8e64383638 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -62,8 +62,7 @@ dependencies { 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" + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 1f7a8d0ee7..ab3cc5fccd 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -58,5 +58,6 @@ (com.google.android.exoplayer2.upstream.DataSource$Factory); } -# Don't warn about checkerframework +# Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** +-dontwarn kotlin.annotations.jvm.** 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 index bd7a70eba0..7678710f18 100644 --- 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 @@ -20,8 +20,8 @@ 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; +import kotlin.annotations.jvm.MigrationStatus; +import kotlin.annotations.jvm.UnderMigration; /** * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless @@ -29,8 +29,6 @@ import javax.annotation.meta.TypeQualifierDefault; */ @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) +@UnderMigration(status = MigrationStatus.STRICT) @Retention(RetentionPolicy.CLASS) public @interface NonNullApi {} From 9f55045eeb07120d5c001db48ef7c7c622089cbd Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Aug 2019 12:08:51 +0100 Subject: [PATCH 306/807] Rollback of https://github.com/google/ExoPlayer/commit/bbe681a904d58fe1f84f6c7c6e3390d932c86249 *** Original commit *** Make Kotlin JVM annotations available and use in ExoPlayer. NoExternal *** PiperOrigin-RevId: 262323737 --- constants.gradle | 1 - library/core/build.gradle | 3 ++- library/core/proguard-rules.txt | 3 +-- .../com/google/android/exoplayer2/util/NonNullApi.java | 8 +++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/constants.gradle b/constants.gradle index 9510b8442e..b1c2c636c7 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,7 +25,6 @@ project.ext { autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' - kotlinAnnotationsVersion = '1.3.31' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/library/core/build.gradle b/library/core/build.gradle index 8e64383638..93126d9830 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -62,7 +62,8 @@ dependencies { compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion - compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + // 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/proguard-rules.txt b/library/core/proguard-rules.txt index ab3cc5fccd..1f7a8d0ee7 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -58,6 +58,5 @@ (com.google.android.exoplayer2.upstream.DataSource$Factory); } -# Don't warn about checkerframework and Kotlin annotations +# Don't warn about checkerframework -dontwarn org.checkerframework.** --dontwarn kotlin.annotations.jvm.** 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 index 7678710f18..bd7a70eba0 100644 --- 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 @@ -20,8 +20,8 @@ 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; +// import kotlin.annotations.jvm.MigrationStatus; +// import kotlin.annotations.jvm.UnderMigration; /** * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless @@ -29,6 +29,8 @@ import kotlin.annotations.jvm.UnderMigration; */ @Nonnull @TypeQualifierDefault(ElementType.TYPE_USE) -@UnderMigration(status = MigrationStatus.STRICT) +// 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 {} From a14df33dc76d628b1f7c1d90fd58128a2dae442d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 8 Aug 2019 16:56:57 +0100 Subject: [PATCH 307/807] Only read from FormatHolder when a format has been read I think we need to start clearing the holder as part of the DRM rework. When we do this, it'll only be valid to read from the holder immediately after it's been populated. PiperOrigin-RevId: 262362725 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../google/android/exoplayer2/metadata/MetadataRenderer.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 5e9d8d0897..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 @@ -847,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++; 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 a775481633..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 @@ -52,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. @@ -120,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); @@ -130,6 +131,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount++; } } + } else if (result == C.RESULT_FORMAT_READ) { + subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; } } From 389eca6e077549fbe740e5975fe93daf573aaa2d Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Aug 2019 16:59:19 +0100 Subject: [PATCH 308/807] Merge robolectric_testutils into testutils. We no longer need two modules as AndroidX-Test takes care of the system abstraction and we no longer have Robolectric Handler/Looper workarounds. PiperOrigin-RevId: 262363201 --- core_settings.gradle | 2 - extensions/cast/build.gradle | 3 +- extensions/cronet/build.gradle | 3 +- extensions/ffmpeg/build.gradle | 3 +- extensions/flac/build.gradle | 3 +- extensions/ima/build.gradle | 3 +- extensions/opus/build.gradle | 3 +- extensions/rtmp/build.gradle | 3 +- extensions/vp9/build.gradle | 3 +- library/core/build.gradle | 1 - library/dash/build.gradle | 3 +- library/hls/build.gradle | 3 +- library/smoothstreaming/build.gradle | 3 +- library/ui/build.gradle | 3 +- testutils/build.gradle | 4 +- .../exoplayer2/testutil/CacheAsserts.java | 0 .../DefaultRenderersFactoryAsserts.java | 0 .../exoplayer2/testutil/FakeMediaChunk.java | 0 .../testutil/FakeMediaChunkIterator.java | 0 .../testutil/FakeMediaClockRenderer.java | 0 .../exoplayer2/testutil/FakeShuffleOrder.java | 0 .../testutil/FakeTrackSelection.java | 0 .../testutil/FakeTrackSelector.java | 0 .../testutil/MediaPeriodAsserts.java | 0 .../testutil/MediaSourceTestRunner.java | 0 .../exoplayer2/testutil/OggTestData.java | 0 .../exoplayer2/testutil/StubExoPlayer.java | 0 .../testutil/TestDownloadManagerListener.java | 0 .../exoplayer2/testutil/TimelineAsserts.java | 0 testutils_robolectric/build.gradle | 46 ------------------- .../src/main/AndroidManifest.xml | 17 ------- 31 files changed, 27 insertions(+), 79 deletions(-) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java (100%) rename {testutils_robolectric => testutils}/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java (100%) delete mode 100644 testutils_robolectric/build.gradle delete mode 100644 testutils_robolectric/src/main/AndroidManifest.xml diff --git a/core_settings.gradle b/core_settings.gradle index 38889e1a21..3f6d58f777 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,7 +24,6 @@ include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' include modulePrefix + 'testutils' -include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' @@ -47,7 +46,6 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') -project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 68a7494a3f..4af8f94c58 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -37,7 +37,8 @@ dependencies { 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/cronet/build.gradle b/extensions/cronet/build.gradle index f7cc707fb4..9c49ba94e1 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -36,7 +36,8 @@ dependencies { 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/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 15952b1860..2b5a6010a9 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -40,7 +40,8 @@ dependencies { implementation project(modulePrefix + 'library-core') 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/flac/build.gradle b/extensions/flac/build.gradle index c67de27697..dfac2e1c26 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -43,7 +43,8 @@ dependencies { 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/ima/build.gradle b/extensions/ima/build.gradle index 0ef9f281c9..41d6aaf628 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -36,7 +36,8 @@ dependencies { 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/opus/build.gradle b/extensions/opus/build.gradle index 28f7b05465..7b621a8df9 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -40,7 +40,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index b74be659ee..74ef70fbf0 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -34,7 +34,8 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 51b2677368..3b8271869b 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -40,7 +40,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + 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/library/core/build.gradle b/library/core/build.gradle index 93126d9830..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/' } } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index c34ed8c907..c64da2b86d 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -43,7 +43,8 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8301820e79..0f685c1130 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -43,7 +43,8 @@ dependencies { 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/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index d85ecbb1a3..b16157f49b 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -43,7 +43,8 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:1.1.0' - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 5182dfccf5..5b3123e302 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -43,7 +43,8 @@ dependencies { implementation 'androidx.media:media:1.0.1' 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/testutils/build.gradle b/testutils/build.gradle index afd2a146af..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.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_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 100% 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 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_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_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_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_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 100% 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 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 100% 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 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_robolectric/build.gradle b/testutils_robolectric/build.gradle deleted file mode 100644 index a098178429..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.1.0' - annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion -} diff --git a/testutils_robolectric/src/main/AndroidManifest.xml b/testutils_robolectric/src/main/AndroidManifest.xml deleted file mode 100644 index 057caad867..0000000000 --- a/testutils_robolectric/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - From 8967dd9c4c48980e433027b8dfb295b1cf99d7cd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Aug 2019 08:33:42 +0100 Subject: [PATCH 309/807] Upgrade IMA dependency version PiperOrigin-RevId: 262511088 --- extensions/ima/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 41d6aaf628..340e9832be 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ 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' From 4e50682fa79a7c0aaf47bb27c02abe4222375dba Mon Sep 17 00:00:00 2001 From: sr1990 Date: Mon, 12 Aug 2019 18:18:12 -0700 Subject: [PATCH 310/807] Support negative value of the @r attrbute of S in SegmentTimeline element --- .../dash/manifest/DashManifestParser.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) 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 8affcb27ce..2d503a8763 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 @@ -237,7 +237,7 @@ public class DashManifestParser extends DefaultHandler seenFirstBaseUrl = true; } } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase)); + adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase, durationMs)); } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { eventStreams.add(parseEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { @@ -245,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, Collections.emptyList()); + segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); } else { maybeSkipTag(xpp); } @@ -262,7 +262,7 @@ public class DashManifestParser extends DefaultHandler // AdaptationSet parsing. protected AdaptationSet parseAdaptationSet( - XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase) + XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase, long periodDurationMs) throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); @@ -328,7 +328,8 @@ public class DashManifestParser extends DefaultHandler roleDescriptors, accessibilityDescriptors, supplementalProperties, - segmentBase); + segmentBase, + periodDurationMs); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -338,7 +339,8 @@ public class DashManifestParser extends DefaultHandler segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = - parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties); + parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties, + periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { @@ -492,7 +494,7 @@ public class DashManifestParser extends DefaultHandler List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, List adaptationSetSupplementalProperties, - @Nullable SegmentBase segmentBase) + @Nullable SegmentBase segmentBase, long periodDurationMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -526,7 +528,8 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate( - xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties); + xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties, + periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -733,7 +736,7 @@ public class DashManifestParser extends DefaultHandler if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { - timeline = parseSegmentTimeline(xpp); + timeline = parseSegmentTimeline(xpp,timescale,duration); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) { if (segments == null) { segments = new ArrayList<>(); @@ -769,7 +772,8 @@ public class DashManifestParser extends DefaultHandler protected SegmentTemplate parseSegmentTemplate( XmlPullParser xpp, @Nullable SegmentTemplate parent, - List adaptationSetSupplementalProperties) + List adaptationSetSupplementalProperties, + long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -792,7 +796,7 @@ public class DashManifestParser extends DefaultHandler if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { - timeline = parseSegmentTimeline(xpp); + timeline = parseSegmentTimeline(xpp,timescale,periodDurationMs); } else { maybeSkipTag(xpp); } @@ -987,7 +991,8 @@ public class DashManifestParser extends DefaultHandler return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } - protected List parseSegmentTimeline(XmlPullParser xpp) + protected List parseSegmentTimeline(XmlPullParser xpp,long timescale, + long periodDurationMs) throws XmlPullParserException, IOException { List segmentTimeline = new ArrayList<>(); long elapsedTime = 0; @@ -996,7 +1001,12 @@ public class DashManifestParser extends DefaultHandler if (XmlPullParserUtil.isStartTag(xpp, "S")) { elapsedTime = parseLong(xpp, "t", elapsedTime); long duration = parseLong(xpp, "d", C.TIME_UNSET); - int count = 1 + parseInt(xpp, "r", 0); + + //if repeat is -1 : length of each segment = duration / timescale and + // number of segments = periodDuration / length of each segment + int repeat = parseInt(xpp,"r",0); + int count = repeat != -1? 1 + repeat : (int) (((periodDurationMs / 1000) * timescale) / duration); + for (int i = 0; i < count; i++) { segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration)); elapsedTime += duration; From e5fcee40e57b35ea9ab3a689d891f7d21ab54f49 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Aug 2019 10:43:26 +0100 Subject: [PATCH 311/807] Make reset on network change the default. PiperOrigin-RevId: 262886490 --- RELEASENOTES.md | 1 + .../android/exoplayer2/upstream/DefaultBandwidthMeter.java | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7c934c478c..c1a9b0f4c7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,7 @@ `DefaultTrackSelector` to allow adaptive selections of audio tracks with different channel counts ([#6257](https://github.com/google/ExoPlayer/issues/6257)). +* Reset `DefaultBandwidthMeter` to initial values on network change. ### 2.10.4 ### 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 4145d9a1c7..9f76ca544f 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 @@ -100,6 +100,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context)); slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT; clock = Clock.DEFAULT; + resetOnNetworkTypeChange = true; } /** @@ -168,14 +169,12 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } /** - * Sets whether to reset if the network type changes. - * - *

          This method is experimental, and will be renamed or removed in a future release. + * Sets whether to reset if the network type changes. The default value is {@code true}. * * @param resetOnNetworkTypeChange Whether to reset if the network type changes. * @return This builder. */ - public Builder experimental_resetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) { + public Builder setResetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) { this.resetOnNetworkTypeChange = resetOnNetworkTypeChange; return this; } From 5fcc4de1fd10f597936582e3a3459ba4ef3fb434 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Mon, 12 Aug 2019 12:56:50 +0100 Subject: [PATCH 312/807] Add SimpleDecoder video base renderer This renderer will be extended by both vp9 and av1 renderers. PiperOrigin-RevId: 262900391 --- .../ext/vp9/LibvpxVideoRenderer.java | 832 ++-------------- .../video/SimpleDecoderVideoRenderer.java | 907 ++++++++++++++++++ 2 files changed, 966 insertions(+), 773 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java 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 b000ea1b6b..696221cae8 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 @@ -18,37 +18,25 @@ package com.google.android.exoplayer2.ext.vp9; import static java.lang.Runtime.getRuntime; import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import androidx.annotation.CallSuper; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import android.view.Surface; -import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlayerMessage.Target; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmSession; -import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.Assertions; 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.SimpleDecoderVideoRenderer; +import com.google.android.exoplayer2.video.VideoDecoderException; import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; -import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; /** * Decodes and renders video using the native VP9 decoder. @@ -64,32 +52,7 @@ import java.lang.annotation.RetentionPolicy; * null. *

        */ -public class LibvpxVideoRenderer extends BaseRenderer { - - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - REINITIALIZATION_STATE_NONE, - REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, - REINITIALIZATION_STATE_WAIT_END_OF_STREAM - }) - private @interface ReinitializationState {} - /** - * The decoder does not need to be re-initialized. - */ - private static final int REINITIALIZATION_STATE_NONE = 0; - /** - * The input format has changed in a way that requires the decoder to be re-initialized, but we - * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to - * ensure that it outputs any remaining buffers before we release it. - */ - private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; - /** - * The input format has changed in a way that requires the decoder to be re-initialized, and we've - * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an - * end of stream signal to indicate that it has output any remaining buffers before we release it. - */ - private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; +public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { /** * The type of a message that can be passed to an instance of this class via {@link @@ -110,51 +73,17 @@ public class LibvpxVideoRenderer extends BaseRenderer { private final boolean enableRowMultiThreadMode; private final boolean disableLoopFilter; - private final long allowedJoiningTimeMs; - private final int maxDroppedFramesToNotify; - private final boolean playClearSamplesWithoutKeys; - private final EventDispatcher eventDispatcher; - private final FormatHolder formatHolder; - private final TimedValueQueue formatQueue; - private final DecoderInputBuffer flagsOnlyBuffer; - private final DrmSessionManager drmSessionManager; private final int threads; - private Format format; - private Format pendingFormat; - private Format outputFormat; - private VpxDecoder decoder; - private VideoDecoderInputBuffer inputBuffer; - private VpxOutputBuffer outputBuffer; - @Nullable private DrmSession decoderDrmSession; - @Nullable private DrmSession sourceDrmSession; - - private @ReinitializationState int decoderReinitializationState; - private boolean decoderReceivedBuffers; - - private boolean renderedFirstFrame; - private long initialPositionUs; - private long joiningDeadlineMs; private Surface surface; private VpxOutputBufferRenderer outputBufferRenderer; @C.VideoOutputMode private int outputMode; - private boolean waitingForKeys; - private boolean inputStreamEnded; - private boolean outputStreamEnded; - private int reportedWidth; - private int reportedHeight; + private VpxDecoder decoder; + private VpxOutputBuffer outputBuffer; - private long droppedFrameAccumulationStartTimeMs; - private int droppedFrames; - private int consecutiveDroppedFrameCount; - private int buffersInCodecCount; - private long lastRenderTimeUs; - private long outputStreamOffsetUs; private VideoFrameMetadataListener frameMetadataListener; - protected DecoderCounters decoderCounters; - /** * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. @@ -259,379 +188,73 @@ public class LibvpxVideoRenderer extends BaseRenderer { int threads, int numInputBuffers, int numOutputBuffers) { - super(C.TRACK_TYPE_VIDEO); + super( + allowedJoiningTimeMs, + eventHandler, + eventListener, + maxDroppedFramesToNotify, + drmSessionManager, + playClearSamplesWithoutKeys); this.disableLoopFilter = disableLoopFilter; - this.allowedJoiningTimeMs = allowedJoiningTimeMs; - this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; - this.drmSessionManager = drmSessionManager; - this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.enableRowMultiThreadMode = enableRowMultiThreadMode; this.threads = threads; this.numInputBuffers = numInputBuffers; this.numOutputBuffers = numOutputBuffers; - joiningDeadlineMs = C.TIME_UNSET; - clearReportedVideoSize(); - formatHolder = new FormatHolder(); - formatQueue = new TimedValueQueue<>(); - flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); - eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = C.VIDEO_OUTPUT_MODE_NONE; - decoderReinitializationState = REINITIALIZATION_STATE_NONE; } - // BaseRenderer implementation. - @Override public int supportsFormat(Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; } - boolean drmIsSupported = - format.drmInitData == null - || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) - || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, format.drmInitData)); - if (!drmIsSupported) { - return FORMAT_UNSUPPORTED_DRM; + if (format.drmInitData == null + || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)) { + return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + return super.supportsFormat(format); } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - return; - } - - if (format == null) { - // We don't have a format yet, so try and read one. - flagsOnlyBuffer.clear(); - int result = readSource(formatHolder, flagsOnlyBuffer, true); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder); - } else if (result == C.RESULT_BUFFER_READ) { - // End of stream read having not read a format. - Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); - inputStreamEnded = true; - outputStreamEnded = true; - return; - } else { - // We still don't have a format and can't make progress without one. - return; - } - } - - // If we don't have a decoder yet, we need to instantiate one. - maybeInitDecoder(); - - if (decoder != null) { - try { - // Rendering loop. - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer()) {} - TraceUtil.endSection(); - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } - decoderCounters.ensureUpdated(); - } - } - - - @Override - public boolean isEnded() { - return outputStreamEnded; + protected SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws VideoDecoderException { + TraceUtil.beginSection("createVpxDecoder"); + int initialInputBufferSize = + format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; + decoder = + new VpxDecoder( + numInputBuffers, + numOutputBuffers, + initialInputBufferSize, + mediaCrypto, + disableLoopFilter, + enableRowMultiThreadMode, + threads); + decoder.setOutputMode(outputMode); + TraceUtil.endSection(); + return decoder; } @Override - public boolean isReady() { - if (waitingForKeys) { - return false; - } - 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; - } else if (joiningDeadlineMs == C.TIME_UNSET) { - // Not joining. - return false; - } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) { - // Joining and still within the joining deadline. - return true; - } else { - // The joining deadline has been exceeded. Give up and clear the deadline. - joiningDeadlineMs = C.TIME_UNSET; - return false; - } + protected VideoDecoderOutputBuffer dequeueOutputBuffer() throws VpxDecoderException { + outputBuffer = decoder.dequeueOutputBuffer(); + return outputBuffer; } @Override - protected void onEnabled(boolean joining) throws ExoPlaybackException { - decoderCounters = new DecoderCounters(); - eventDispatcher.enabled(decoderCounters); - } - - @Override - protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - inputStreamEnded = false; - outputStreamEnded = false; - clearRenderedFirstFrame(); - initialPositionUs = C.TIME_UNSET; - consecutiveDroppedFrameCount = 0; - if (decoder != null) { - flushDecoder(); - } - if (joining) { - setJoiningDeadlineMs(); - } else { - joiningDeadlineMs = C.TIME_UNSET; - } - formatQueue.clear(); - } - - @Override - protected void onStarted() { - droppedFrames = 0; - droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; - } - - @Override - protected void onStopped() { - joiningDeadlineMs = C.TIME_UNSET; - maybeNotifyDroppedFrames(); - } - - @Override - protected void onDisabled() { - format = null; - waitingForKeys = false; - clearReportedVideoSize(); - clearRenderedFirstFrame(); - try { - setSourceDrmSession(null); - releaseDecoder(); - } finally { - eventDispatcher.disabled(decoderCounters); - } - } - - @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { - outputStreamOffsetUs = offsetUs; - super.onStreamChanged(formats, offsetUs); - } - - /** - * Called when a decoder has been created and configured. - * - *

        The default implementation is a no-op. - * - * @param name The name of the decoder that was initialized. - * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization - * finished. - * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. - */ - @CallSuper - protected void onDecoderInitialized( - String name, long initializedTimestampMs, long initializationDurationMs) { - eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); - } - - /** - * Flushes the decoder. - * - * @throws ExoPlaybackException If an error occurs reinitializing a decoder. - */ - @CallSuper - protected void flushDecoder() throws ExoPlaybackException { - waitingForKeys = false; - buffersInCodecCount = 0; - if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { - releaseDecoder(); - maybeInitDecoder(); - } else { - inputBuffer = null; - if (outputBuffer != null) { - outputBuffer.release(); - outputBuffer = null; - } - decoder.flush(); - decoderReceivedBuffers = false; - } - } - - /** Releases the decoder. */ - @CallSuper - protected void releaseDecoder() { - inputBuffer = null; - outputBuffer = null; - decoderReinitializationState = REINITIALIZATION_STATE_NONE; - decoderReceivedBuffers = false; - buffersInCodecCount = 0; - if (decoder != null) { - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; - } - setDecoderDrmSession(null); - } - - private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(sourceDrmSession, session); - sourceDrmSession = session; - } - - private void setDecoderDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(decoderDrmSession, session); - decoderDrmSession = session; - } - - /** - * Called when a new format is read from the upstream source. - * - * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. - * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. - */ - @CallSuper - @SuppressWarnings("unchecked") - protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { - Format oldFormat = format; - format = formatHolder.format; - pendingFormat = format; - - boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null - : oldFormat.drmInitData); - if (drmInitDataChanged) { - if (format.drmInitData != null) { - 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; - } - } else { - setSourceDrmSession(null); - } + protected void renderOutputBuffer(long presentationTimeUs, Format outputFormat) + throws VpxDecoderException { + if (frameMetadataListener != null) { + frameMetadataListener.onVideoFrameAboutToBeRendered( + presentationTimeUs, System.nanoTime(), outputFormat); } - if (sourceDrmSession != decoderDrmSession) { - if (decoderReceivedBuffers) { - // Signal end of stream and wait for any final output buffers before re-initialization. - decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; - } else { - // There aren't any final output buffers, so release the decoder immediately. - releaseDecoder(); - maybeInitDecoder(); - } - } - - eventDispatcher.inputFormatChanged(format); - } - - /** - * Called immediately before an input buffer is queued into the decoder. - * - *

        The default implementation is a no-op. - * - * @param buffer The buffer that will be queued. - */ - protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { - // Do nothing. - } - - /** - * Called when an output buffer is successfully processed. - * - * @param presentationTimeUs The timestamp associated with the output buffer. - */ - @CallSuper - protected void onProcessedOutputBuffer(long presentationTimeUs) { - buffersInCodecCount--; - } - - /** - * Returns whether the buffer being processed should be dropped. - * - * @param earlyUs The time until the buffer should be presented in microseconds. A 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. - */ - protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - return isBufferLate(earlyUs); - } - - /** - * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after - * the current playback position, if possible. - * - * @param earlyUs The time until the current buffer should be presented in microseconds. A - * 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. - */ - protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { - return isBufferVeryLate(earlyUs); - } - - /** - * Returns whether to force rendering an output buffer. - * - * @param earlyUs The time until the current buffer should be presented in microseconds. A - * negative value indicates that the buffer is late. - * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in - * microseconds. - * @return Returns whether to force rendering an output buffer. - */ - protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { - return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; - } - - /** - * Skips the specified output buffer and releases it. - * - * @param outputBuffer The output buffer to skip. - */ - protected void skipOutputBuffer(VpxOutputBuffer outputBuffer) { - decoderCounters.skippedOutputBufferCount++; - outputBuffer.release(); - } - - /** - * Drops the specified output buffer and releases it. - * - * @param outputBuffer The output buffer to drop. - */ - protected void dropOutputBuffer(VpxOutputBuffer outputBuffer) { - updateDroppedBufferCounters(1); - outputBuffer.release(); - } - - /** - * Renders the specified output buffer. - * - *

        The implementation of this method takes ownership of the output buffer and is responsible - * for calling {@link VpxOutputBuffer#release()} either immediately or in the future. - * - * @param outputBuffer The buffer to render. - */ - protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException { int bufferMode = outputBuffer.mode; 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); } else { @@ -643,49 +266,19 @@ public class LibvpxVideoRenderer extends BaseRenderer { decoder.renderToSurface(outputBuffer, surface); outputBuffer.release(); } - consecutiveDroppedFrameCount = 0; - decoderCounters.renderedOutputBufferCount++; - maybeNotifyRenderedFirstFrame(); + onFrameRendered(surface); } } - /** - * Drops frames from the current output buffer to the next keyframe at or before the playback - * position. If no such keyframe exists, as the playback position is inside the same group of - * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. - * - * @param positionUs The current playback position, in microseconds. - * @return Whether any buffers were dropped. - * @throws ExoPlaybackException If an error occurs flushing the decoder. - */ - protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { - int droppedSourceBufferCount = skipSource(positionUs); - if (droppedSourceBufferCount == 0) { - return false; - } - decoderCounters.droppedToKeyframeCount++; - // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, - // which releases all pending buffers buffers including the current output buffer. - updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); - flushDecoder(); - return true; + @Override + protected void clearOutputBuffer() { + super.clearOutputBuffer(); + outputBuffer = null; } - /** - * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were - * dropped. - * - * @param droppedBufferCount The number of additional dropped buffers. - */ - protected void updateDroppedBufferCounters(int droppedBufferCount) { - decoderCounters.droppedBufferCount += droppedBufferCount; - droppedFrames += droppedBufferCount; - consecutiveDroppedFrameCount += droppedBufferCount; - decoderCounters.maxConsecutiveDroppedBufferCount = - Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); - if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) { - maybeNotifyDroppedFrames(); - } + @Override + protected boolean hasOutputSurface() { + return outputMode != C.VIDEO_OUTPUT_MODE_NONE; } // PlayerMessage.Target implementation. @@ -719,325 +312,18 @@ public class LibvpxVideoRenderer extends BaseRenderer { outputMode = outputBufferRenderer != null ? C.VIDEO_OUTPUT_MODE_YUV : C.VIDEO_OUTPUT_MODE_NONE; } - if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) { + if (hasOutputSurface()) { if (decoder != null) { decoder.setOutputMode(outputMode); } - // If we know the video size, report it again immediately. - maybeRenotifyVideoSizeChanged(); - // We haven't rendered to the new output yet. - clearRenderedFirstFrame(); - if (getState() == STATE_STARTED) { - setJoiningDeadlineMs(); - } + onOutputSurfaceChanged(); } else { // The output has been removed. We leave the outputMode of the underlying decoder unchanged // in anticipation that a subsequent output will likely be of the same type. - clearReportedVideoSize(); - clearRenderedFirstFrame(); + onOutputSurfaceRemoved(); } - } 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(); - maybeRenotifyRenderedFirstFrame(); + } else if (hasOutputSurface()) { + onOutputSurfaceReset(surface); } } - - private void maybeInitDecoder() throws ExoPlaybackException { - if (decoder != null) { - return; - } - - setDecoderDrmSession(sourceDrmSession); - - ExoMediaCrypto mediaCrypto = null; - if (decoderDrmSession != null) { - mediaCrypto = decoderDrmSession.getMediaCrypto(); - if (mediaCrypto == null) { - DrmSessionException drmError = decoderDrmSession.getError(); - if (drmError != null) { - // Continue for now. We may be able to avoid failure if the session recovers, or if a new - // input format causes the session to be replaced before it's used. - } else { - // The drm session isn't open yet. - return; - } - } - } - - try { - long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createVpxDecoder"); - int initialInputBufferSize = - format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; - decoder = - new VpxDecoder( - numInputBuffers, - numOutputBuffers, - initialInputBufferSize, - mediaCrypto, - disableLoopFilter, - enableRowMultiThreadMode, - threads); - decoder.setOutputMode(outputMode); - TraceUtil.endSection(); - long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); - onDecoderInitialized( - decoder.getName(), - decoderInitializedTimestamp, - decoderInitializedTimestamp - decoderInitializingTimestamp); - decoderCounters.decoderInitCount++; - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } - } - - private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { - if (decoder == null - || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM - || inputStreamEnded) { - // We need to reinitialize the decoder or the input stream has ended. - return false; - } - - if (inputBuffer == null) { - inputBuffer = decoder.dequeueInputBuffer(); - if (inputBuffer == null) { - return false; - } - } - - if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { - inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; - return false; - } - - int result; - if (waitingForKeys) { - // We've already read an encrypted sample into buffer, and are waiting for keys. - result = C.RESULT_BUFFER_READ; - } else { - result = readSource(formatHolder, inputBuffer, false); - } - - if (result == C.RESULT_NOTHING_READ) { - return false; - } - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder); - return true; - } - if (inputBuffer.isEndOfStream()) { - inputStreamEnded = true; - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - return false; - } - boolean bufferEncrypted = inputBuffer.isEncrypted(); - waitingForKeys = shouldWaitForKeys(bufferEncrypted); - if (waitingForKeys) { - return false; - } - if (pendingFormat != null) { - formatQueue.add(inputBuffer.timeUs, pendingFormat); - pendingFormat = null; - } - inputBuffer.flip(); - inputBuffer.colorInfo = format.colorInfo; - onQueueInputBuffer(inputBuffer); - decoder.queueInputBuffer(inputBuffer); - buffersInCodecCount++; - decoderReceivedBuffers = true; - decoderCounters.inputBufferCount++; - inputBuffer = null; - return true; - } - - /** - * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link - * #processOutputBuffer(long, long)}. - * - * @param positionUs The player's current position. - * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, - * measured at the start of the current iteration of the rendering loop. - * @return Whether it may be possible to drain more output data. - * @throws ExoPlaybackException If an error occurs draining the output buffer. - */ - private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) - throws ExoPlaybackException, VpxDecoderException { - if (outputBuffer == null) { - outputBuffer = decoder.dequeueOutputBuffer(); - if (outputBuffer == null) { - return false; - } - decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; - buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; - } - - if (outputBuffer.isEndOfStream()) { - if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { - // We're waiting to re-initialize the decoder, and have now processed all final buffers. - releaseDecoder(); - maybeInitDecoder(); - } else { - outputBuffer.release(); - outputBuffer = null; - outputStreamEnded = true; - } - return false; - } - - boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs); - if (processedOutputBuffer) { - onProcessedOutputBuffer(outputBuffer.timeUs); - outputBuffer = null; - } - return processedOutputBuffer; - } - - /** - * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns - * whether it may be possible to process another output buffer. - * - * @param positionUs The player's current position. - * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, - * measured at the start of the current iteration of the rendering loop. - * @return Whether it may be possible to drain another output buffer. - * @throws ExoPlaybackException If an error occurs processing the output buffer. - */ - private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) - throws ExoPlaybackException, VpxDecoderException { - if (initialPositionUs == C.TIME_UNSET) { - initialPositionUs = positionUs; - } - - long earlyUs = outputBuffer.timeUs - positionUs; - 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); - return true; - } - return false; - } - - long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs; - Format format = formatQueue.pollFloor(presentationTimeUs); - if (format != null) { - outputFormat = format; - } - - long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; - boolean isStarted = getState() == STATE_STARTED; - if (!renderedFirstFrame - || (isStarted - && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { - if (frameMetadataListener != null) { - frameMetadataListener.onVideoFrameAboutToBeRendered( - presentationTimeUs, System.nanoTime(), outputFormat); - } - renderOutputBuffer(outputBuffer); - return true; - } - - if (!isStarted || positionUs == initialPositionUs) { - return false; - } - - if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) - && maybeDropBuffersToKeyframe(positionUs)) { - return false; - } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { - dropOutputBuffer(outputBuffer); - return true; - } - - if (earlyUs < 30000) { - if (frameMetadataListener != null) { - frameMetadataListener.onVideoFrameAboutToBeRendered( - presentationTimeUs, System.nanoTime(), outputFormat); - } - renderOutputBuffer(outputBuffer); - return true; - } - - return false; - } - - private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { - return false; - } - @DrmSession.State int drmSessionState = decoderDrmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); - } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; - } - - private void setJoiningDeadlineMs() { - joiningDeadlineMs = allowedJoiningTimeMs > 0 - ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; - } - - private void clearRenderedFirstFrame() { - renderedFirstFrame = false; - } - - private void maybeNotifyRenderedFirstFrame() { - if (!renderedFirstFrame) { - renderedFirstFrame = true; - eventDispatcher.renderedFirstFrame(surface); - } - } - - private void maybeRenotifyRenderedFirstFrame() { - if (renderedFirstFrame) { - eventDispatcher.renderedFirstFrame(surface); - } - } - - private void clearReportedVideoSize() { - reportedWidth = Format.NO_VALUE; - reportedHeight = Format.NO_VALUE; - } - - private void maybeNotifyVideoSizeChanged(int width, int height) { - if (reportedWidth != width || reportedHeight != height) { - reportedWidth = width; - reportedHeight = height; - eventDispatcher.videoSizeChanged(width, height, 0, 1); - } - } - - private void maybeRenotifyVideoSizeChanged() { - if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { - eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, 0, 1); - } - } - - private void maybeNotifyDroppedFrames() { - if (droppedFrames > 0) { - long now = SystemClock.elapsedRealtime(); - long elapsedMs = now - droppedFrameAccumulationStartTimeMs; - eventDispatcher.droppedFrames(droppedFrames, elapsedMs); - droppedFrames = 0; - droppedFrameAccumulationStartTimeMs = now; - } - } - - private static boolean isBufferLate(long earlyUs) { - // Class a buffer as late if it should have been presented more than 30 ms ago. - return earlyUs < -30000; - } - - private static boolean isBufferVeryLate(long earlyUs) { - // Class a buffer as very late if it should have been presented more than 500 ms ago. - return earlyUs < -500000; - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java new file mode 100644 index 0000000000..d66155548d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -0,0 +1,907 @@ +/* + * 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 android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; +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.VideoRendererEventListener.EventDispatcher; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Decodes and renders video using a {@link SimpleDecoder}. */ +public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { + + /** Decoder reinitialization states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + REINITIALIZATION_STATE_NONE, + REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM + }) + private @interface ReinitializationState {} + /** The decoder does not need to be re-initialized. */ + private static final int REINITIALIZATION_STATE_NONE = 0; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, but we + * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to + * ensure that it outputs any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, and we've + * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an + * end of stream signal to indicate that it has output any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + + private final long allowedJoiningTimeMs; + private final int maxDroppedFramesToNotify; + private final boolean playClearSamplesWithoutKeys; + private final EventDispatcher eventDispatcher; + private final FormatHolder formatHolder; + private final TimedValueQueue formatQueue; + private final DecoderInputBuffer flagsOnlyBuffer; + private final DrmSessionManager drmSessionManager; + + private Format format; + private Format pendingFormat; + private Format outputFormat; + private SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + decoder; + private VideoDecoderInputBuffer inputBuffer; + private VideoDecoderOutputBuffer outputBuffer; + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; + + @ReinitializationState private int decoderReinitializationState; + private boolean decoderReceivedBuffers; + + private boolean renderedFirstFrame; + private long initialPositionUs; + private long joiningDeadlineMs; + private boolean waitingForKeys; + + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private int reportedWidth; + private int reportedHeight; + + private long droppedFrameAccumulationStartTimeMs; + private int droppedFrames; + private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; + private long lastRenderTimeUs; + private long outputStreamOffsetUs; + + /** Decoder event counters used for debugging purposes. */ + protected DecoderCounters decoderCounters; + + /** + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media 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. + */ + protected SimpleDecoderVideoRenderer( + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { + super(C.TRACK_TYPE_VIDEO); + this.allowedJoiningTimeMs = allowedJoiningTimeMs; + this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + joiningDeadlineMs = C.TIME_UNSET; + clearReportedVideoSize(); + formatHolder = new FormatHolder(); + formatQueue = new TimedValueQueue<>(); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); + eventDispatcher = new EventDispatcher(eventHandler, eventListener); + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + } + + // BaseRenderer implementation. + + @Override + public int supportsFormat(Format format) { + boolean drmIsSupported = + format.drmInitData == null + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); + if (!drmIsSupported) { + return FORMAT_UNSUPPORTED_DRM; + } + return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + + if (format == null) { + // We don't have a format yet, so try and read one. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + outputStreamEnded = true; + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } + } + + // If we don't have a decoder yet, we need to instantiate one. + maybeInitDecoder(); + + if (decoder != null) { + try { + // Rendering loop. + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer()) {} + TraceUtil.endSection(); + } catch (VideoDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + decoderCounters.ensureUpdated(); + } + } + + @Override + public boolean isEnded() { + return outputStreamEnded; + } + + @Override + public boolean isReady() { + if (waitingForKeys) { + return false; + } + if (format != null + && (isSourceReady() || outputBuffer != null) + && (renderedFirstFrame || !hasOutputSurface())) { + // Ready. If we were joining then we've now joined, so clear the joining deadline. + joiningDeadlineMs = C.TIME_UNSET; + return true; + } else if (joiningDeadlineMs == C.TIME_UNSET) { + // Not joining. + return false; + } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) { + // Joining and still within the joining deadline. + return true; + } else { + // The joining deadline has been exceeded. Give up and clear the deadline. + joiningDeadlineMs = C.TIME_UNSET; + return false; + } + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + decoderCounters = new DecoderCounters(); + eventDispatcher.enabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + inputStreamEnded = false; + outputStreamEnded = false; + clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; + consecutiveDroppedFrameCount = 0; + if (decoder != null) { + flushDecoder(); + } + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } + formatQueue.clear(); + } + + @Override + protected void onStarted() { + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + } + + @Override + protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; + maybeNotifyDroppedFrames(); + } + + @Override + protected void onDisabled() { + format = null; + waitingForKeys = false; + clearReportedVideoSize(); + clearRenderedFirstFrame(); + try { + setSourceDrmSession(null); + releaseDecoder(); + } finally { + eventDispatcher.disabled(decoderCounters); + } + } + + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + outputStreamOffsetUs = offsetUs; + super.onStreamChanged(formats, offsetUs); + } + + /** + * Called when a decoder has been created and configured. + * + *

        The default implementation is a no-op. + * + * @param name The name of the decoder that was initialized. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. + * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. + */ + @CallSuper + protected void onDecoderInitialized( + String name, long initializedTimestampMs, long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + } + + /** + * Flushes the decoder. + * + * @throws ExoPlaybackException If an error occurs reinitializing a decoder. + */ + @CallSuper + protected void flushDecoder() throws ExoPlaybackException { + waitingForKeys = false; + buffersInCodecCount = 0; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + clearOutputBuffer(); + } + decoder.flush(); + decoderReceivedBuffers = false; + } + } + + /** Releases the decoder. */ + @CallSuper + protected void releaseDecoder() { + inputBuffer = null; + clearOutputBuffer(); + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + decoderReceivedBuffers = false; + buffersInCodecCount = 0; + if (decoder != null) { + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + } + setDecoderDrmSession(null); + } + + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSessionReferences(sourceDrmSession, session); + sourceDrmSession = session; + } + + private void setDecoderDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSessionReferences(decoderDrmSession, session); + decoderDrmSession = session; + } + + /** + * Called when a new format is read from the upstream source. + * + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. + * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. + */ + @CallSuper + @SuppressWarnings("unchecked") + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + Format oldFormat = format; + format = formatHolder.format; + pendingFormat = format; + + boolean drmInitDataChanged = + !Util.areEqual(format.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); + if (drmInitDataChanged) { + if (format.drmInitData != null) { + 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; + } + } else { + setSourceDrmSession(null); + } + } + + if (sourceDrmSession != decoderDrmSession) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + } + } + + eventDispatcher.inputFormatChanged(format); + } + + /** + * Called immediately before an input buffer is queued into the decoder. + * + *

        The default implementation is a no-op. + * + * @param buffer The buffer that will be queued. + */ + protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { + // Do nothing. + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + protected void onProcessedOutputBuffer(long presentationTimeUs) { + buffersInCodecCount--; + } + + /** + * Returns whether the buffer being processed should be dropped. + * + * @param earlyUs The time until the buffer should be presented in microseconds. A 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. + */ + protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { + return isBufferLate(earlyUs); + } + + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * 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. + */ + protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { + return isBufferVeryLate(earlyUs); + } + + /** + * Returns whether to force rendering an output buffer. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in + * microseconds. + * @return Returns whether to force rendering an output buffer. + */ + protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { + return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; + } + + /** + * Skips the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to skip. + */ + protected void skipOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + decoderCounters.skippedOutputBufferCount++; + outputBuffer.release(); + } + + /** + * Drops the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to drop. + */ + protected void dropOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + updateDroppedBufferCounters(1); + outputBuffer.release(); + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param positionUs The current playback position, in microseconds. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the decoder. + */ + protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushDecoder(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = + Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); + if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + } + + /** + * Creates a decoder for the given format. + * + * @param format The format for which a decoder is required. + * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. + * May be null and can be ignored if decoder does not handle encrypted content. + * @return The decoder. + * @throws VideoDecoderException If an error occurred creating a suitable decoder. + */ + protected abstract SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws VideoDecoderException; + + /** + * Dequeues output buffer. + * + * @return Dequeued video decoder output buffer. + * @throws VideoDecoderException If an error occurs while dequeuing the output buffer. + */ + protected abstract VideoDecoderOutputBuffer dequeueOutputBuffer() throws VideoDecoderException; + + /** Clears output buffer. */ + protected void clearOutputBuffer() { + outputBuffer = null; + } + + /** + * Renders the specified output buffer. + * + *

        The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. + * + * @param presentationTimeUs Presentation time in microseconds. + * @param outputFormat Output format. + */ + // TODO: The output buffer is not being passed to this method currently. Due to the need of + // decoder-specific output buffer type, the reference to the output buffer is being kept in the + // subclass. Once the common output buffer is established, this method can be updated to receive + // the output buffer as an argument. See [Internal: b/139174707]. + protected abstract void renderOutputBuffer(long presentationTimeUs, Format outputFormat) + throws VideoDecoderException; + + /** + * Returns whether the renderer has output surface. + * + * @return Whether the renderer has output surface. + */ + protected abstract boolean hasOutputSurface(); + + /** Called when the output surface is changed. */ + protected final void onOutputSurfaceChanged() { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new output yet. + clearRenderedFirstFrame(); + if (getState() == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } + + /** Called when the output surface is removed. */ + protected final void onOutputSurfaceRemoved() { + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + + /** + * Called when the output surface is set again to the same non-null value. + * + * @param surface Output surface. + */ + protected final void onOutputSurfaceReset(Surface surface) { + // 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(); + maybeRenotifyRenderedFirstFrame(surface); + } + + /** + * Notifies event dispatcher if video size changed. + * + * @param width New video width. + * @param height New video height. + */ + protected final void maybeNotifyVideoSizeChanged(int width, int height) { + if (reportedWidth != width || reportedHeight != height) { + reportedWidth = width; + reportedHeight = height; + eventDispatcher.videoSizeChanged( + width, height, /* unappliedRotationDegrees= */ 0, /* pixelWidthHeightRatio= */ 1); + } + } + + /** Called after rendering a frame. */ + protected final void onFrameRendered(Surface surface) { + consecutiveDroppedFrameCount = 0; + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(surface); + } + + // Internal methods. + + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; + } + + setDecoderDrmSession(sourceDrmSession); + + ExoMediaCrypto mediaCrypto = null; + if (decoderDrmSession != null) { + mediaCrypto = decoderDrmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = decoderDrmSession.getError(); + if (drmError != null) { + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; + } + } + } + + try { + long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); + decoder = createDecoder(format, mediaCrypto); + long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); + onDecoderInitialized( + decoder.getName(), + decoderInitializedTimestamp, + decoderInitializedTimestamp - decoderInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VideoDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + } + + private boolean feedInputBuffer() throws VideoDecoderException, ExoPlaybackException { + if (decoder == null + || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + + int result; + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer, false); + } + + if (result == C.RESULT_NOTHING_READ) { + return false; + } + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return false; + } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } + if (pendingFormat != null) { + formatQueue.add(inputBuffer.timeUs, pendingFormat); + pendingFormat = null; + } + inputBuffer.flip(); + inputBuffer.colorInfo = format.colorInfo; + onQueueInputBuffer(inputBuffer); + decoder.queueInputBuffer(inputBuffer); + buffersInCodecCount++; + decoderReceivedBuffers = true; + decoderCounters.inputBufferCount++; + inputBuffer = null; + return true; + } + + /** + * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link + * #processOutputBuffer(long, long)}. + * + * @param positionUs The player's current position. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @return Whether it may be possible to drain more output data. + * @throws ExoPlaybackException If an error occurs draining the output buffer. + */ + private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException, VideoDecoderException { + if (outputBuffer == null) { + outputBuffer = dequeueOutputBuffer(); + if (outputBuffer == null) { + return false; + } + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; + } + + if (outputBuffer.isEndOfStream()) { + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + clearOutputBuffer(); + outputStreamEnded = true; + } + return false; + } + + boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs); + if (processedOutputBuffer) { + onProcessedOutputBuffer(outputBuffer.timeUs); + clearOutputBuffer(); + } + return processedOutputBuffer; + } + + /** + * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns + * whether it may be possible to process another output buffer. + * + * @param positionUs The player's current position. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @return Whether it may be possible to drain another output buffer. + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException, VideoDecoderException { + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; + } + + long earlyUs = outputBuffer.timeUs - positionUs; + if (!hasOutputSurface()) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(outputBuffer); + return true; + } + return false; + } + + long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs; + Format format = formatQueue.pollFloor(presentationTimeUs); + if (format != null) { + outputFormat = format; + } + + long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + boolean isStarted = getState() == STATE_STARTED; + if (!renderedFirstFrame + || (isStarted + && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + renderOutputBuffer(presentationTimeUs, outputFormat); + return true; + } + + if (!isStarted || positionUs == initialPositionUs) { + return false; + } + + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + && maybeDropBuffersToKeyframe(positionUs)) { + return false; + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + dropOutputBuffer(outputBuffer); + return true; + } + + if (earlyUs < 30000) { + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + renderOutputBuffer(presentationTimeUs, outputFormat); + return true; + } + + return false; + } + + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + return false; + } + @DrmSession.State int drmSessionState = decoderDrmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; + } + + private void setJoiningDeadlineMs() { + joiningDeadlineMs = + allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) + : C.TIME_UNSET; + } + + private void clearRenderedFirstFrame() { + renderedFirstFrame = false; + } + + private void maybeNotifyRenderedFirstFrame(Surface surface) { + if (!renderedFirstFrame) { + renderedFirstFrame = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void maybeRenotifyRenderedFirstFrame(Surface surface) { + if (renderedFirstFrame) { + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void clearReportedVideoSize() { + reportedWidth = Format.NO_VALUE; + reportedHeight = Format.NO_VALUE; + } + + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { + eventDispatcher.videoSizeChanged( + reportedWidth, + reportedHeight, + /* unappliedRotationDegrees= */ 0, + /* pixelWidthHeightRatio= */ 1); + } + } + + private void maybeNotifyDroppedFrames() { + if (droppedFrames > 0) { + long now = SystemClock.elapsedRealtime(); + long elapsedMs = now - droppedFrameAccumulationStartTimeMs; + eventDispatcher.droppedFrames(droppedFrames, elapsedMs); + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = now; + } + } + + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30 ms ago. + return earlyUs < -30000; + } + + private static boolean isBufferVeryLate(long earlyUs) { + // Class a buffer as very late if it should have been presented more than 500 ms ago. + return earlyUs < -500000; + } +} From 79c4f1878e308b2095087b8b363eadd5d15d284d Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Aug 2019 16:39:16 +0100 Subject: [PATCH 313/807] Fix JavaDoc generation errors. This fixes the errors that prevent the JavaDoc generation with the Gradle script to run through. PiperOrigin-RevId: 262930857 --- .../android/exoplayer2/source/CompositeMediaSource.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 4ebe97313b..7077416a02 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 @@ -96,11 +96,11 @@ public abstract class CompositeMediaSource extends BaseMediaSource { /** * Prepares a child source. * - *

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

        {@link #onChildSourceInfoRefreshed(Object, 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(T)} will - * be released in {@link #releaseSourceInternal()}. + *

        Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} + * 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}. From 9f0fd870e7c82d703ec0957ac47bb971aa763956 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 13 Aug 2019 02:10:57 +0100 Subject: [PATCH 314/807] Add haveRenderedFirstFrame PiperOrigin-RevId: 263046027 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 5 +++++ 1 file changed, 5 insertions(+) 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 2ab7e61378..7061521b53 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 @@ -1638,6 +1638,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return surface; } + /** Returns true if the first frame has been rendered (playback has not necessarily begun). */ + protected final boolean haveRenderedFirstFrame() { + return renderedFirstFrame; + } + protected static final class CodecMaxValues { public final int width; From 860aa2f952468b777cb691e45da8f7cbc8700398 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Tue, 13 Aug 2019 11:25:54 +0100 Subject: [PATCH 315/807] Remove video decoder buffers from nullness blacklist PiperOrigin-RevId: 263104935 --- .../exoplayer2/video/VideoDecoderInputBuffer.java | 3 ++- .../exoplayer2/video/VideoDecoderOutputBuffer.java | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java index 76742a8691..360279c11c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java @@ -15,12 +15,13 @@ */ package com.google.android.exoplayer2.video; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; /** Input buffer to a video decoder. */ public class VideoDecoderInputBuffer extends DecoderInputBuffer { - public ColorInfo colorInfo; + @Nullable public ColorInfo colorInfo; 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 index af0844defb..b4b09b20a2 100644 --- 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.video; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.OutputBuffer; import java.nio.ByteBuffer; @@ -33,16 +34,16 @@ public abstract class VideoDecoderOutputBuffer extends OutputBuffer { /** Output mode. */ @C.VideoOutputMode public int mode; /** RGB buffer for RGB mode. */ - public ByteBuffer data; + @Nullable public ByteBuffer data; public int width; public int height; - public ColorInfo colorInfo; + @Nullable public ColorInfo colorInfo; /** YUV planes for YUV mode. */ - public ByteBuffer[] yuvPlanes; + @Nullable public ByteBuffer[] yuvPlanes; - public int[] yuvStrides; + @Nullable public int[] yuvStrides; public int colorspace; /** @@ -88,6 +89,10 @@ public abstract class VideoDecoderOutputBuffer extends OutputBuffer { if (yuvPlanes == null) { yuvPlanes = new ByteBuffer[3]; } + + ByteBuffer data = this.data; + ByteBuffer[] yuvPlanes = this.yuvPlanes; + // Rewrapping has to be done on every frame since the stride might have changed. yuvPlanes[0] = data.slice(); yuvPlanes[0].limit(yLength); From 0e33123938368b6606d1d55496e6b746465a42f6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 13 Aug 2019 15:45:43 +0100 Subject: [PATCH 316/807] Turn on non-null-by-default for some core library packages. And add missing some missing annotations to the publicly visible API of these packages. PiperOrigin-RevId: 263134804 --- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 7 ++++--- .../exoplayer2/ext/flac/FlacDecoder.java | 3 ++- .../exoplayer2/ext/opus/OpusDecoder.java | 5 +++-- .../exoplayer2/ext/vp9/VpxDecoder.java | 3 ++- .../android/exoplayer2/BaseRenderer.java | 2 ++ .../android/exoplayer2/NoSampleRenderer.java | 2 ++ .../google/android/exoplayer2/Renderer.java | 11 ++++++----- .../exoplayer2/analytics/package-info.java | 19 +++++++++++++++++++ .../android/exoplayer2/audio/Ac3Util.java | 4 ++-- .../android/exoplayer2/audio/Ac4Util.java | 3 ++- .../exoplayer2/audio/AudioAttributes.java | 2 +- .../android/exoplayer2/audio/DtsUtil.java | 3 ++- .../audio/MediaCodecAudioRenderer.java | 8 +++++--- .../exoplayer2/audio/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/database/package-info.java | 19 +++++++++++++++++++ .../android/exoplayer2/decoder/Decoder.java | 4 ++++ .../decoder/DecoderInputBuffer.java | 9 +++++---- .../exoplayer2/decoder/SimpleDecoder.java | 2 ++ .../decoder/SimpleOutputBuffer.java | 3 ++- .../exoplayer2/decoder/package-info.java | 19 +++++++++++++++++++ .../android/exoplayer2/drm/package-info.java | 19 +++++++++++++++++++ .../mediacodec/MediaCodecRenderer.java | 12 +++++++----- .../exoplayer2/mediacodec/MediaCodecUtil.java | 1 + .../exoplayer2/mediacodec/package-info.java | 19 +++++++++++++++++++ .../metadata/emsg/EventMessageDecoder.java | 2 +- .../exoplayer2/metadata/icy/IcyDecoder.java | 3 ++- .../exoplayer2/metadata/id3/Id3Decoder.java | 3 ++- .../android/exoplayer2/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/scheduler/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/source/SilenceMediaSource.java | 2 +- .../trackselection/package-info.java | 19 +++++++++++++++++++ .../android/exoplayer2/util/AtomicFile.java | 5 ++--- .../exoplayer2/util/HandlerWrapper.java | 7 ++++--- .../exoplayer2/util/SystemHandlerWrapper.java | 7 ++++--- .../android/exoplayer2/util/UriUtil.java | 9 +++++---- .../google/android/exoplayer2/util/Util.java | 8 ++++---- .../android/exoplayer2/util/package-info.java | 17 +++++++++++++++++ .../video/MediaCodecVideoRenderer.java | 8 +++++--- .../video/SimpleDecoderVideoRenderer.java | 3 ++- .../exoplayer2/video/package-info.java | 19 +++++++++++++++++++ .../video/spherical/CameraMotionRenderer.java | 2 +- .../video/spherical/package-info.java | 19 +++++++++++++++++++ .../source/dash/EventSampleStream.java | 2 +- 43 files changed, 314 insertions(+), 57 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/database/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/decoder/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java 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 c78b02aa5b..5314835d1e 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; 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.util.List; @@ -106,7 +107,7 @@ import java.util.List; return new FfmpegDecoderException("Error resetting (see logcat)."); } } - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Util.castNonNull(inputBuffer.data); int inputSize = inputData.limit(); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); @@ -132,8 +133,8 @@ import java.util.List; } hasOutputFormat = true; } - outputBuffer.data.position(0); - outputBuffer.data.limit(result); + outputData.position(0); + outputData.limit(result); return null; } 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 50eb048d98..890d82a006 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 @@ -22,6 +22,7 @@ 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.FlacStreamMetadata; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; @@ -101,7 +102,7 @@ import java.util.List; if (reset) { decoderJni.flush(); } - decoderJni.setData(inputBuffer.data); + decoderJni.setData(Util.castNonNull(inputBuffer.data)); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); try { decoderJni.decodeSample(outputData); 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 d93036113c..f0e993e3b9 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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; @@ -165,7 +166,7 @@ import java.util.List; // any other time, skip number of samples as specified by seek preroll. skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples; } - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Util.castNonNull(inputBuffer.data); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; int result = inputBuffer.isEncrypted() ? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), @@ -185,7 +186,7 @@ import java.util.List; } } - ByteBuffer outputData = outputBuffer.data; + ByteBuffer outputData = Util.castNonNull(outputBuffer.data); outputData.position(0); outputData.limit(result); if (skipSamples > 0) { 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 0efd4bd0ea..1392e782f8 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 @@ -22,6 +22,7 @@ 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.util.Util; import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; import java.nio.ByteBuffer; @@ -118,7 +119,7 @@ import java.nio.ByteBuffer; @Nullable protected VpxDecoderException decode( VideoDecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Util.castNonNull(inputBuffer.data); int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; final long result = inputBuffer.isEncrypted() diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 1099b14bfc..f5db0145fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -65,6 +65,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -105,6 +106,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override + @Nullable public final SampleStream getStream() { return stream; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index e901025a07..894736571c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -49,6 +49,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -113,6 +114,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities } @Override + @Nullable public final SampleStream getStream() { return stream; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 9f52e8d9de..9e44e3741c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; @@ -87,11 +88,12 @@ public interface Renderer extends PlayerMessage.Target { /** * If the renderer advances its own playback position then this method returns a corresponding * {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its - * source of time during playback. A player may have at most one renderer that returns a - * {@link MediaClock} from this method. + * source of time during playback. A player may have at most one renderer that returns a {@link + * MediaClock} from this method. * * @return The {@link MediaClock} tracking the playback position of the renderer, or null. */ + @Nullable MediaClock getMediaClock(); /** @@ -147,9 +149,8 @@ public interface Renderer extends PlayerMessage.Target { void replaceStream(Format[] formats, SampleStream stream, long offsetUs) throws ExoPlaybackException; - /** - * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. - */ + /** Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. */ + @Nullable SampleStream getStream(); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/package-info.java new file mode 100644 index 0000000000..2764120d2a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/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.analytics; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 4e4964e817..05c20939ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -156,7 +156,7 @@ public final class Ac3Util { * @return The AC-3 format parsed from data in the header. */ public static Format parseAc3AnnexFFormat( - ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -189,7 +189,7 @@ public final class Ac3Util { * @return The E-AC-3 format parsed from data in the header. */ public static Format parseEAc3AnnexFFormat( - ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { data.skipBytes(2); // data_rate, num_ind_sub // Read the first independent substream. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java index 74bd5bfe98..c54e3844a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -95,7 +96,7 @@ public final class Ac4Util { * @return The AC-4 format parsed from data in the header. */ public static Format parseAc4AnnexEFormat( - ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { data.skipBytes(1); // ac4_dsi_version, bitstream_version[0:5] int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100; return Format.createAudioSampleFormat( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index 9c63eb42c6..1b0d629da7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -92,7 +92,7 @@ public final class AudioAttributes { public final @C.AudioFlags int flags; public final @C.AudioUsage int usage; - private @Nullable android.media.AudioAttributes audioAttributesV21; + @Nullable private android.media.AudioAttributes audioAttributesV21; private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags, @C.AudioUsage int usage) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index f65dc3fc4e..7af9d9f074 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.util.MimeTypes; @@ -80,7 +81,7 @@ public final class DtsUtil { * @return The DTS format parsed from data in the header. */ public static Format parseDtsFormat( - byte[] frame, String trackId, String language, DrmInitData drmInitData) { + byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { ParsableBitArray frameBits = getNormalizedFrameHeader(frame); frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); 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 6a29f316e1..251901f4f2 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 @@ -300,8 +300,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, Format format) + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { @@ -386,7 +388,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto, + @Nullable MediaCrypto crypto, float codecOperatingRate) { codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/package-info.java new file mode 100644 index 0000000000..5ae2413d92 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/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.audio; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/database/package-info.java new file mode 100644 index 0000000000..4921e1aeea --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/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.database; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java index 7eb1fa1aa1..4552d190c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.decoder; +import androidx.annotation.Nullable; + /** * A media decoder. * @@ -37,6 +39,7 @@ public interface Decoder { * @return The input buffer, which will have been cleared, or null if a buffer isn't available. * @throws E If a decoder error has occurred. */ + @Nullable I dequeueInputBuffer() throws E; /** @@ -53,6 +56,7 @@ public interface Decoder { * @return The output buffer, or null if an output buffer isn't available. * @throws E If a decoder error has occurred. */ + @Nullable O dequeueOutputBuffer() throws E; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 7fc6fb625a..c31ae92cfc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.decoder; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Holds input for a decoder. @@ -58,10 +60,8 @@ public class DecoderInputBuffer extends Buffer { */ public final CryptoInfo cryptoInfo; - /** - * The buffer's data, or {@code null} if no data has been set. - */ - public ByteBuffer data; + /** The buffer's data, or {@code null} if no data has been set. */ + @Nullable public ByteBuffer data; /** * The time at which the sample should be presented. @@ -101,6 +101,7 @@ public class DecoderInputBuffer extends Buffer { * @throws IllegalStateException If there is insufficient capacity to accommodate the write and * the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. */ + @EnsuresNonNull("data") public void ensureSpaceForWrite(int length) { if (data == null) { data = createReplacementByteBuffer(length); 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 b5650860e9..b7465f82eb 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 @@ -86,6 +86,7 @@ public abstract class SimpleDecoder< } @Override + @Nullable public final I dequeueInputBuffer() throws E { synchronized (lock) { maybeThrowException(); @@ -108,6 +109,7 @@ public abstract class SimpleDecoder< } @Override + @Nullable public final O dequeueOutputBuffer() throws E { synchronized (lock) { maybeThrowException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java index 49c7dafbd6..84cffc1145 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.decoder; +import androidx.annotation.Nullable; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -25,7 +26,7 @@ public class SimpleOutputBuffer extends OutputBuffer { private final SimpleDecoder owner; - public ByteBuffer data; + @Nullable public ByteBuffer data; public SimpleOutputBuffer(SimpleDecoder owner) { this.owner = owner; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/package-info.java new file mode 100644 index 0000000000..0c4dbde9d3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/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.decoder; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/package-info.java new file mode 100644 index 0000000000..d4820dd204 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/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.drm; + +import com.google.android.exoplayer2.util.NonNullApi; 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 974e033b67..ee2c9ad1a3 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 @@ -452,12 +452,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param mediaCodecSelector The decoder selector. * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. See - * {@link #supportsFormat(Format)} for more detail. + * @return The extent to which the renderer is capable of supporting the given format. See {@link + * #supportsFormat(Format)} for more detail. * @throws DecoderQueryException If there was an error querying decoders. */ - protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, Format format) + protected abstract int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException; /** @@ -487,7 +489,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto, + @Nullable MediaCrypto crypto, float codecOperatingRate); protected final void maybeInitCodec() throws ExoPlaybackException { 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 cd4c4863ff..9c42916cad 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 @@ -239,6 +239,7 @@ public final class MediaCodecUtil { * @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(Format format) { if (format.codecs == null) { return null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java new file mode 100644 index 0000000000..b09404a6f8 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/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.mediacodec; + +import com.google.android.exoplayer2.util.NonNullApi; 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 f592a6eee7..a1196c41c8 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 @@ -29,7 +29,7 @@ public final class EventMessageDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); byte[] data = buffer.array(); int size = buffer.limit(); return new Metadata(decode(new ParsableByteArray(data, size))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 3d873926bb..12f65f1cda 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; @@ -39,7 +40,7 @@ public final class IcyDecoder implements MetadataDecoder { @Nullable @SuppressWarnings("ByteBufferBackingArray") public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); byte[] data = buffer.array(); int length = buffer.limit(); return decode(Util.fromUtf8Bytes(data, 0, length)); 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 85a59c3aeb..c8755f9aee 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 @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; 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.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -99,7 +100,7 @@ public final class Id3Decoder implements MetadataDecoder { @Override @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); return decode(buffer.array(), buffer.limit()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/package-info.java new file mode 100644 index 0000000000..690f2c40c3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/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; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java new file mode 100644 index 0000000000..6273f325c4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/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.scheduler; + +import com.google.android.exoplayer2.util.NonNullApi; 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 index c3eab68983..3bb7ada7e0 100644 --- 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 @@ -220,9 +220,9 @@ public final class SilenceMediaSource extends BaseMediaSource { 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); + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); positionBytes += bytesToWrite; return C.RESULT_BUFFER_READ; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java new file mode 100644 index 0000000000..45131e644b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/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.trackselection; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 74e50dfd92..f2259e8f1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import androidx.annotation.NonNull; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -189,12 +188,12 @@ public final class AtomicFile { } @Override - public void write(@NonNull byte[] b) throws IOException { + public void write(byte[] b) throws IOException { fileOutputStream.write(b); } @Override - public void write(@NonNull byte[] b, int off, int len) throws IOException { + public void write(byte[] b, int off, int len) throws IOException { fileOutputStream.write(b, off, len); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index 8f1a6544ca..5b85b26c3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import android.os.Handler; import android.os.Looper; import android.os.Message; +import androidx.annotation.Nullable; /** * An interface to call through to a {@link Handler}. Instances must be created by calling {@link @@ -32,13 +33,13 @@ public interface HandlerWrapper { Message obtainMessage(int what); /** @see Handler#obtainMessage(int, Object) */ - Message obtainMessage(int what, Object obj); + Message obtainMessage(int what, @Nullable Object obj); /** @see Handler#obtainMessage(int, int, int) */ Message obtainMessage(int what, int arg1, int arg2); /** @see Handler#obtainMessage(int, int, int, Object) */ - Message obtainMessage(int what, int arg1, int arg2, Object obj); + Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj); /** @see Handler#sendEmptyMessage(int) */ boolean sendEmptyMessage(int what); @@ -50,7 +51,7 @@ public interface HandlerWrapper { void removeMessages(int what); /** @see Handler#removeCallbacksAndMessages(Object) */ - void removeCallbacksAndMessages(Object token); + void removeCallbacksAndMessages(@Nullable Object token); /** @see Handler#post(Runnable) */ boolean post(Runnable runnable); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index ee469a5b2a..1fbea2ed7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import android.os.Looper; import android.os.Message; +import androidx.annotation.Nullable; /** The standard implementation of {@link HandlerWrapper}. */ /* package */ final class SystemHandlerWrapper implements HandlerWrapper { @@ -38,7 +39,7 @@ import android.os.Message; } @Override - public Message obtainMessage(int what, Object obj) { + public Message obtainMessage(int what, @Nullable Object obj) { return handler.obtainMessage(what, obj); } @@ -48,7 +49,7 @@ import android.os.Message; } @Override - public Message obtainMessage(int what, int arg1, int arg2, Object obj) { + public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) { return handler.obtainMessage(what, arg1, arg2, obj); } @@ -68,7 +69,7 @@ import android.os.Message; } @Override - public void removeCallbacksAndMessages(Object token) { + public void removeCallbacksAndMessages(@Nullable Object token) { handler.removeCallbacksAndMessages(token); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java index 071ebf2084..60f4fa17dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.util; import android.net.Uri; +import androidx.annotation.Nullable; import android.text.TextUtils; /** @@ -69,19 +70,19 @@ public final class UriUtil { * @param baseUri The base URI. * @param referenceUri The reference URI to resolve. */ - public static Uri resolveToUri(String baseUri, String referenceUri) { + public static Uri resolveToUri(@Nullable String baseUri, @Nullable String referenceUri) { return Uri.parse(resolve(baseUri, referenceUri)); } /** * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}. - *

        - * The resolution is performed as specified by RFC-3986. + * + *

        The resolution is performed as specified by RFC-3986. * * @param baseUri The base URI. * @param referenceUri The reference URI to resolve. */ - public static String resolve(String baseUri, String referenceUri) { + public static String resolve(@Nullable String baseUri, @Nullable String referenceUri) { StringBuilder uri = new StringBuilder(); // Map null onto empty string, to make the following logic simpler. 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 e700fc6751..6db5ddef39 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 @@ -252,14 +252,14 @@ public final class Util { /** * Tests whether an {@code items} array contains an object equal to {@code item}, according to * {@link Object#equals(Object)}. - *

        - * If {@code item} is null then true is returned if and only if {@code items} contains null. + * + *

        If {@code item} is null then true is returned if and only if {@code items} contains null. * * @param items The array of items to search. * @param item The item to search for. * @return True if the array contains an object equal to the item being searched for. */ - public static boolean contains(Object[] items, Object item) { + public static boolean contains(@NullableType Object[] items, @Nullable Object item) { for (Object arrayItem : items) { if (areEqual(arrayItem, item)) { return true; @@ -1486,7 +1486,7 @@ public final class Util { * @return The content type. */ @C.ContentType - public static int inferContentType(Uri uri, String overrideExtension) { + public static int inferContentType(Uri uri, @Nullable String overrideExtension) { return TextUtils.isEmpty(overrideExtension) ? inferContentType(uri) : inferContentType("." + overrideExtension); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/util/package-info.java new file mode 100644 index 0000000000..76899fc452 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/package-info.java @@ -0,0 +1,17 @@ +/* + * 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.util; 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 7061521b53..de77e8318d 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 @@ -308,8 +308,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, Format format) + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { @@ -601,7 +603,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto, + @Nullable MediaCrypto crypto, float codecOperatingRate) { String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index d66155548d..2dd43fb6d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -544,9 +544,10 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { /** * Dequeues output buffer. * - * @return Dequeued video decoder output buffer. + * @return Dequeued video decoder output buffer, or null if an output buffer isn't available. * @throws VideoDecoderException If an error occurs while dequeuing the output buffer. */ + @Nullable protected abstract VideoDecoderOutputBuffer dequeueOutputBuffer() throws VideoDecoderException; /** Clears output buffer. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/video/package-info.java new file mode 100644 index 0000000000..3c2cd217e0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/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.video; + +import com.google.android.exoplayer2.util.NonNullApi; 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 03822be17c..7d76f43d04 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 @@ -93,7 +93,7 @@ public class CameraMotionRenderer extends BaseRenderer { buffer.flip(); lastTimestampUs = buffer.timeUs; if (listener != null) { - float[] rotation = parseMetadata(buffer.data); + float[] rotation = parseMetadata(Util.castNonNull(buffer.data)); if (rotation != null) { Util.castNonNull(listener).onCameraMotion(lastTimestampUs - offsetUs, rotation); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java new file mode 100644 index 0000000000..2dce6889d8 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/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.video.spherical; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java index f06a709960..6e67be6ec5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -115,9 +115,9 @@ import java.io.IOException; byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex]); if (serializedEvent != null) { buffer.ensureSpaceForWrite(serializedEvent.length); - buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); buffer.data.put(serializedEvent); buffer.timeUs = eventTimesUs[sampleIndex]; + buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); return C.RESULT_BUFFER_READ; } else { return C.RESULT_NOTHING_READ; From b77b9f5c024b0cf24edc91135f9c177bedb62987 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Tue, 13 Aug 2019 17:18:51 +0100 Subject: [PATCH 317/807] Update dequeueOutputBuffer method Add @Nullable annotation in the LibvpxVideoRenderer. PiperOrigin-RevId: 263150736 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 696221cae8..4c20cd7a82 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 @@ -239,6 +239,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { } @Override + @Nullable protected VideoDecoderOutputBuffer dequeueOutputBuffer() throws VpxDecoderException { outputBuffer = decoder.dequeueOutputBuffer(); return outputBuffer; From 81a290f1ee7c36144de53215e7be974664526f28 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Wed, 14 Aug 2019 10:48:18 +0100 Subject: [PATCH 318/807] Add internal method for format support PiperOrigin-RevId: 263312721 --- .../ext/vp9/LibvpxVideoRenderer.java | 15 ++++++++---- .../video/SimpleDecoderVideoRenderer.java | 23 +++++++++++-------- 2 files changed, 24 insertions(+), 14 deletions(-) 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 4c20cd7a82..fb68a36949 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 @@ -204,15 +204,20 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { } @Override - public int supportsFormat(Format format) { + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; } - if (format.drmInitData == null - || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)) { - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + boolean drmIsSupported = + format.drmInitData == null + || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); + if (!drmIsSupported) { + return FORMAT_UNSUPPORTED_DRM; } - return super.supportsFormat(format); + return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 2dd43fb6d6..3d18242466 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; @@ -155,15 +156,8 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // BaseRenderer implementation. @Override - public int supportsFormat(Format format) { - boolean drmIsSupported = - format.drmInitData == null - || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, format.drmInitData)); - if (!drmIsSupported) { - return FORMAT_UNSUPPORTED_DRM; - } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + public final int supportsFormat(Format format) { + return supportsFormatInternal(drmSessionManager, format); } @Override @@ -526,6 +520,17 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } } + /** + * Returns the extent to which the subclass supports a given format. + * + * @param drmSessionManager The renderer's {@link DrmSessionManager}. + * @param format The format, which has a video {@link Format#sampleMimeType}. + * @return The extent to which the subclass supports the format itself. + * @see RendererCapabilities#supportsFormat(Format) + */ + protected abstract int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format); + /** * Creates a decoder for the given format. * From 213912b328b5b8bef4199bae0cbedb7b6baa8447 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Wed, 14 Aug 2019 11:32:04 +0100 Subject: [PATCH 319/807] Update default input buffer size documentation PiperOrigin-RevId: 263317893 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 fb68a36949..aea422176e 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 @@ -68,8 +68,11 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * requiring multiple output buffers to be dequeued at a time for it to make progress. */ private final int numOutputBuffers; - /** The default input buffer size. */ - private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. + /** + * The default input buffer size. The value is based on SoftVPX.cpp. + */ + private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; private final boolean enableRowMultiThreadMode; private final boolean disableLoopFilter; From a572fb3f22d73c60ce5ab3eaa7db9dd1e3356dcb Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Aug 2019 12:37:44 +0100 Subject: [PATCH 320/807] Add a metadata argument to Format factory methods used in HLS Required for propagation of HlsMetadataEntry's in chunkless preparation. PiperOrigin-RevId: 263324345 --- .../com/google/android/exoplayer2/Format.java | 16 ++++++++++------ .../source/dash/manifest/DashManifestParser.java | 2 ++ .../exoplayer2/source/dash/DashUtilTest.java | 1 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 ++ .../source/hls/playlist/HlsPlaylistParser.java | 3 +++ .../source/hls/HlsMediaPeriodTest.java | 3 +++ .../manifest/SsManifestParser.java | 2 ++ 7 files changed, 23 insertions(+), 6 deletions(-) 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 b2bd20f0fe..37539845dc 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 @@ -180,8 +180,8 @@ public final class Format implements Parcelable { // Video. /** - * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, int, - * int, int, float, List, int, int)} instead. + * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, + * Metadata, int, int, int, float, List, int, int)} instead. */ @Deprecated public static Format createVideoContainerFormat( @@ -201,6 +201,7 @@ public final class Format implements Parcelable { containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, width, height, @@ -216,6 +217,7 @@ public final class Format implements Parcelable { @Nullable String containerMimeType, String sampleMimeType, String codecs, + @Nullable Metadata metadata, int bitrate, int width, int height, @@ -230,7 +232,7 @@ public final class Format implements Parcelable { roleFlags, bitrate, codecs, - /* metadata= */ null, + metadata, containerMimeType, sampleMimeType, /* maxInputSize= */ NO_VALUE, @@ -363,8 +365,8 @@ public final class Format implements Parcelable { // Audio. /** - * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, int, - * int, int, List, int, int, String)} instead. + * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, + * Metadata, int, int, int, List, int, int, String)} instead. */ @Deprecated public static Format createAudioContainerFormat( @@ -384,6 +386,7 @@ public final class Format implements Parcelable { containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, channelCount, sampleRate, @@ -399,6 +402,7 @@ public final class Format implements Parcelable { @Nullable String containerMimeType, @Nullable String sampleMimeType, @Nullable String codecs, + @Nullable Metadata metadata, int bitrate, int channelCount, int sampleRate, @@ -413,7 +417,7 @@ public final class Format implements Parcelable { roleFlags, bitrate, codecs, - /* metadata= */ null, + metadata, containerMimeType, sampleMimeType, /* maxInputSize= */ NO_VALUE, 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 8affcb27ce..9f6cce672e 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 @@ -596,6 +596,7 @@ public class DashManifestParser extends DefaultHandler containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, width, height, @@ -610,6 +611,7 @@ public class DashManifestParser extends DefaultHandler containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, audioChannels, audioSamplingRate, diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index a53b1ff80d..6e769b72e1 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -80,6 +80,7 @@ public final class DashUtilTest { MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, /* codecs= */ "", + /* metadata= */ null, Format.NO_VALUE, /* width= */ 1024, /* height= */ 768, 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 8053958c2b..f4f91cf1b4 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 @@ -787,6 +787,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, variantFormat.bitrate, variantFormat.width, variantFormat.height, @@ -829,6 +830,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, channelCount, /* sampleRate= */ Format.NO_VALUE, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 42b27f259f..030520f8cb 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -349,6 +349,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser { MimeTypes.VIDEO_MP4, sampleMimeType, /* codecs= */ null, + /* metadata= */ null, bitrate, width, height, @@ -706,6 +707,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { MimeTypes.AUDIO_MP4, sampleMimeType, /* codecs= */ null, + /* metadata= */ null, bitrate, channels, samplingRate, From 6dd3d49093377290b8a506cca866b8c6db68f77d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Aug 2019 13:09:21 +0100 Subject: [PATCH 321/807] Change default video buffer size to 32MB. The current max video buffer is 13MB which is too small for high quality streams and doesn't allow the DefaultLoadControl to buffer up to its default max buffer time of 50 seconds. Also move util method and constants only used by DefaultLoadControl into this class. PiperOrigin-RevId: 263328088 --- RELEASENOTES.md | 2 + .../java/com/google/android/exoplayer2/C.java | 19 --------- .../exoplayer2/DefaultLoadControl.java | 42 ++++++++++++++++++- .../google/android/exoplayer2/util/Util.java | 29 ------------- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c1a9b0f4c7..2fe1cf8a54 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,8 @@ different channel counts ([#6257](https://github.com/google/ExoPlayer/issues/6257)). * Reset `DefaultBandwidthMeter` to initial values on network change. +* Increase maximum buffer size for video in `DefaultLoadControl` to ensure high + quality video can be loaded up to the full default buffer duration. ### 2.10.4 ### 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 daa6124df6..cd862e503f 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 @@ -685,25 +685,6 @@ public final class C { /** A default size in bytes for an individual allocation that forms part of a larger buffer. */ public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - /** A default size in bytes for a video buffer. */ - public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for an audio buffer. */ - public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a text buffer. */ - public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a metadata buffer. */ - public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a camera motion buffer. */ - public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */ - public static final int DEFAULT_MUXED_BUFFER_SIZE = - DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; - /** "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. */ @SuppressWarnings("ConstantField") public static final String CENC_TYPE_cenc = "cenc"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 972f651a41..1244b96d94 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -67,6 +67,25 @@ public class DefaultLoadControl implements LoadControl { /** The default for whether the back buffer is retained from the previous keyframe. */ public static final boolean DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false; + /** A default size in bytes for a video buffer. */ + public static final int DEFAULT_VIDEO_BUFFER_SIZE = 500 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for an audio buffer. */ + public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a text buffer. */ + public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a metadata buffer. */ + public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a camera motion buffer. */ + public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */ + public static final int DEFAULT_MUXED_BUFFER_SIZE = + DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + /** Builder for {@link DefaultLoadControl}. */ public static final class Builder { @@ -404,7 +423,7 @@ public class DefaultLoadControl implements LoadControl { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { if (trackSelectionArray.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType()); } } return targetBufferSize; @@ -418,6 +437,27 @@ public class DefaultLoadControl implements LoadControl { } } + private static int getDefaultBufferSize(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_DEFAULT: + return DEFAULT_MUXED_BUFFER_SIZE; + case C.TRACK_TYPE_AUDIO: + return DEFAULT_AUDIO_BUFFER_SIZE; + case C.TRACK_TYPE_VIDEO: + return DEFAULT_VIDEO_BUFFER_SIZE; + case C.TRACK_TYPE_TEXT: + return DEFAULT_TEXT_BUFFER_SIZE; + case C.TRACK_TYPE_METADATA: + return DEFAULT_METADATA_BUFFER_SIZE; + case C.TRACK_TYPE_CAMERA_MOTION: + return DEFAULT_CAMERA_MOTION_BUFFER_SIZE; + case C.TRACK_TYPE_NONE: + return 0; + default: + throw new IllegalArgumentException(); + } + } + private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) { for (int i = 0; i < renderers.length; i++) { if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) { 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 6db5ddef39..35a36c3cd5 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 @@ -1545,35 +1545,6 @@ public final class Util { : formatter.format("%02d:%02d", minutes, seconds).toString(); } - /** - * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} {@code - * DEFAULT_*_BUFFER_SIZE} constant. - * - * @param trackType The track type. - * @return The corresponding default buffer size in bytes. - * @throws IllegalArgumentException If the track type is an unrecognized or custom track type. - */ - public static int getDefaultBufferSize(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_DEFAULT: - return C.DEFAULT_MUXED_BUFFER_SIZE; - case C.TRACK_TYPE_AUDIO: - return C.DEFAULT_AUDIO_BUFFER_SIZE; - case C.TRACK_TYPE_VIDEO: - return C.DEFAULT_VIDEO_BUFFER_SIZE; - case C.TRACK_TYPE_TEXT: - return C.DEFAULT_TEXT_BUFFER_SIZE; - case C.TRACK_TYPE_METADATA: - return C.DEFAULT_METADATA_BUFFER_SIZE; - case C.TRACK_TYPE_CAMERA_MOTION: - return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE; - case C.TRACK_TYPE_NONE: - return 0; - default: - throw new IllegalArgumentException(); - } - } - /** * Escapes a string so that it's safe for use as a file or directory name on at least FAT32 * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today. From 5482bd05e40e1b23dffe6b3f426cef2ac9a8a629 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 14 Aug 2019 14:12:51 +0100 Subject: [PATCH 322/807] Add description to TextInformationFrame.toString() output This field is used in .equals(), we should print it in toString() too PiperOrigin-RevId: 263335432 --- .../android/exoplayer2/metadata/id3/TextInformationFrame.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0e129ca7bb..8337911c0d 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 @@ -66,7 +66,7 @@ public final class TextInformationFrame extends Id3Frame { @Override public String toString() { - return id + ": value=" + value; + return id + ": description=" + description + ": value=" + value; } // Parcelable implementation. From 69965ddf26f2855ebebd52825714d085c2e514fa Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 14 Aug 2019 14:13:32 +0100 Subject: [PATCH 323/807] Add Metadata.toString that prints the contents of `entries` entries are used in .equals(), so it's good to have them printed in toString() too (for test failures) and it makes logging easier too. PiperOrigin-RevId: 263335503 --- .../com/google/android/exoplayer2/metadata/Metadata.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 7b4f4c0836..dbc1114bd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -122,6 +122,11 @@ public final class Metadata implements Parcelable { return Arrays.hashCode(entries); } + @Override + public String toString() { + return "entries=" + Arrays.toString(entries); + } + // Parcelable implementation. @Override From cd4571161ae2bbe97788a2bf8ba51ed0580d407d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Aug 2019 14:41:40 +0100 Subject: [PATCH 324/807] Add builders for SimpleExoPlayer and ExoPlayer. The current ExoPlayerFactory is growing too big and usage becomes increasingly complicated because it's not possible to set individual components without specifying many other defaults. Adding new builder classes makes building easier and more future-proof. PiperOrigin-RevId: 263339078 --- RELEASENOTES.md | 2 + .../exoplayer2/castdemo/PlayerManager.java | 3 +- .../exoplayer2/gvrdemo/PlayerActivity.java | 5 +- .../exoplayer2/imademo/PlayerManager.java | 3 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- extensions/ffmpeg/README.md | 20 +- extensions/flac/README.md | 20 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 6 +- extensions/opus/README.md | 20 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 6 +- extensions/vp9/README.md | 20 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 6 +- .../exoplayer2/DefaultRenderersFactory.java | 6 +- .../google/android/exoplayer2/ExoPlayer.java | 165 ++++++++++- .../android/exoplayer2/ExoPlayerFactory.java | 267 ++++-------------- .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../android/exoplayer2/SimpleExoPlayer.java | 228 ++++++++++++++- .../AdaptiveTrackSelection.java | 8 +- .../trackselection/DefaultTrackSelector.java | 3 +- .../upstream/DefaultBandwidthMeter.java | 15 + .../playbacktests/gts/DashTestRunner.java | 12 +- .../exoplayer2/testutil/ExoHostedTest.java | 17 +- 22 files changed, 543 insertions(+), 298 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2fe1cf8a54..bf89c83724 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ * Reset `DefaultBandwidthMeter` to initial values on network change. * Increase maximum buffer size for video in `DefaultLoadControl` to ensure high quality video can be loaded up to the full default buffer duration. +* Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and + `ExoPlayer.Builder`. ### 2.10.4 ### diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 8b75eb0c74..b1a12f6bc9 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -20,7 +20,6 @@ import android.net.Uri; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.EventListener; @@ -119,7 +118,7 @@ import java.util.Map; mediaDrms = new IdentityHashMap<>(); trackSelector = new DefaultTrackSelector(context); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); exoPlayer.addListener(this); localPlayerView.setPlayer(exoPlayer); 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 index 059f26b374..15cc9b6469 100644 --- 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 @@ -24,7 +24,6 @@ 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; @@ -138,7 +137,9 @@ public class PlayerActivity extends GvrPlayerActivity implements PlaybackPrepare lastSeenTrackGroupArray = null; player = - ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector); + new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setTrackSelector(trackSelector) + .build(); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 8f2c891e3a..3caf7f0c16 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -20,7 +20,6 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.MediaSource; @@ -54,7 +53,7 @@ import com.google.android.exoplayer2.util.Util; public void init(Context context, PlayerView playerView) { // Create a player instance. - player = ExoPlayerFactory.newSimpleInstance(context); + player = new SimpleExoPlayer.Builder(context).build(); adsLoader.setPlayer(player); playerView.setPlayer(player); 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 1e231dd45e..d3c32ac957 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 @@ -33,7 +33,6 @@ import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; @@ -371,7 +370,9 @@ public class PlayerActivity extends AppCompatActivity lastSeenTrackGroupArray = null; player = - ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector); + new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) + .setTrackSelector(trackSelector) + .build(); player.addListener(new PlayerEventListener()); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 5b68f1e352..dd9ce38d35 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -122,22 +122,22 @@ Once you've followed the instructions above to check out, build and depend on the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`. How you do this depends on which player API you're using: -* If you're passing a `DefaultRenderersFactory` to - `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by - setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` - constructor to `EXTENSION_RENDERER_MODE_ON`. This will use - `FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't - support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give - `FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`, + you can enable using the extension by setting the `extensionRendererMode` + parameter of the `DefaultRenderersFactory` constructor to + `EXTENSION_RENDERER_MODE_ON`. This will use `FfmpegAudioRenderer` for playback + if `MediaCodecAudioRenderer` doesn't support the input format. Pass + `EXTENSION_RENDERER_MODE_PREFER` to give `FfmpegAudioRenderer` priority over + `MediaCodecAudioRenderer`. * If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer` to the output list in `buildAudioRenderers`. ExoPlayer will use the first `Renderer` in the list that supports the input media format. * If you've implemented your own `RenderersFactory`, return an `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the first `Renderer` in the returned array that supports the input media format. -* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer` - in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the - list that supports the input media format. +* If you're using `ExoPlayer.Builder`, pass an `FfmpegAudioRenderer` in the + array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that + supports the input media format. Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 78035f4d87..b4b0dae002 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -68,22 +68,22 @@ renderer. ### Using `LibflacAudioRenderer` ### -* If you're passing a `DefaultRenderersFactory` to - `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by - setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` - constructor to `EXTENSION_RENDERER_MODE_ON`. This will use - `LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't - support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give - `LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`, + you can enable using the extension by setting the `extensionRendererMode` + parameter of the `DefaultRenderersFactory` constructor to + `EXTENSION_RENDERER_MODE_ON`. This will use `LibflacAudioRenderer` for + playback if `MediaCodecAudioRenderer` doesn't support the input format. Pass + `EXTENSION_RENDERER_MODE_PREFER` to give `LibflacAudioRenderer` priority over + `MediaCodecAudioRenderer`. * If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer` to the output list in `buildAudioRenderers`. ExoPlayer will use the first `Renderer` in the list that supports the input media format. * If you've implemented your own `RenderersFactory`, return a `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the first `Renderer` in the returned array that supports the input media format. -* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer` - in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the - list that supports the input media format. +* If you're using `ExoPlayer.Builder`, pass a `LibflacAudioRenderer` in the + array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that + supports the input media format. Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, 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 c10d6fdb27..bf96442f61 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 @@ -24,13 +24,10 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import org.junit.Before; import org.junit.Test; @@ -82,8 +79,7 @@ public class FlacPlaybackTest { public void run() { Looper.prepare(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); - player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); + player = new ExoPlayer.Builder(context, audioRenderer).build(); player.addListener(this); MediaSource mediaSource = new ProgressiveMediaSource.Factory( diff --git a/extensions/opus/README.md b/extensions/opus/README.md index 95c6807275..af44e84b04 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -71,22 +71,22 @@ Once you've followed the instructions above to check out, build and depend on the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`. How you do this depends on which player API you're using: -* If you're passing a `DefaultRenderersFactory` to - `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by - setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` - constructor to `EXTENSION_RENDERER_MODE_ON`. This will use - `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't - support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give - `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`, + you can enable using the extension by setting the `extensionRendererMode` + parameter of the `DefaultRenderersFactory` constructor to + `EXTENSION_RENDERER_MODE_ON`. This will use `LibopusAudioRenderer` for + playback if `MediaCodecAudioRenderer` doesn't support the input format. Pass + `EXTENSION_RENDERER_MODE_PREFER` to give `LibopusAudioRenderer` priority over + `MediaCodecAudioRenderer`. * If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer` to the output list in `buildAudioRenderers`. ExoPlayer will use the first `Renderer` in the list that supports the input media format. * If you've implemented your own `RenderersFactory`, return a `LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the first `Renderer` in the returned array that supports the input media format. -* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer` - in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the - list that supports the input media format. +* If you're using `ExoPlayer.Builder`, pass a `LibopusAudioRenderer` in the + array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that + supports the input media format. Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, 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 382ee38e06..b3d5b525d5 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 @@ -24,13 +24,10 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import org.junit.Before; import org.junit.Test; @@ -82,8 +79,7 @@ public class OpusPlaybackTest { public void run() { Looper.prepare(); LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer(); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); - player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); + player = new ExoPlayer.Builder(context, audioRenderer).build(); player.addListener(this); MediaSource mediaSource = new ProgressiveMediaSource.Factory( diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index be75eae359..34230db2ec 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -85,22 +85,22 @@ Once you've followed the instructions above to check out, build and depend on the extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`. How you do this depends on which player API you're using: -* If you're passing a `DefaultRenderersFactory` to - `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by - setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` - constructor to `EXTENSION_RENDERER_MODE_ON`. This will use - `LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't - support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER` - to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`. +* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`, + you can enable using the extension by setting the `extensionRendererMode` + parameter of the `DefaultRenderersFactory` constructor to + `EXTENSION_RENDERER_MODE_ON`. This will use `LibvpxVideoRenderer` for playback + if `MediaCodecVideoRenderer` doesn't support decoding the input VP9 stream. + Pass `EXTENSION_RENDERER_MODE_PREFER` to give `LibvpxVideoRenderer` priority + over `MediaCodecVideoRenderer`. * If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer` to the output list in `buildVideoRenderers`. ExoPlayer will use the first `Renderer` in the list that supports the input media format. * If you've implemented your own `RenderersFactory`, return a `LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the first `Renderer` in the returned array that supports the input media format. -* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer` - in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the - list that supports the input media format. +* If you're using `ExoPlayer.Builder`, pass a `LibvpxVideoRenderer` in the array + of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that + supports the input media format. Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, 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 9be1d9c0e5..d4e0795293 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 @@ -25,13 +25,10 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Log; import org.junit.Before; @@ -115,8 +112,7 @@ public class VpxPlaybackTest { public void run() { Looper.prepare(); LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); - player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector); + player = new ExoPlayer.Builder(context, videoRenderer).build(); player.addListener(this); MediaSource mediaSource = new ProgressiveMediaSource.Factory( 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 490d961396..a97b1e0d5a 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 @@ -104,7 +104,7 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager} - * directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * directly to {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -127,7 +127,7 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link - * SimpleExoPlayer} or {@link ExoPlayerFactory}. + * SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -154,7 +154,7 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass - * {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * {@link DrmSessionManager} directly to {@link SimpleExoPlayer.Builder}. */ @Deprecated public DefaultRenderersFactory( 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 ee29af9c99..27c3a630f8 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 @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2; +import android.content.Context; import android.os.Looper; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; @@ -29,12 +31,17 @@ import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link - * ExoPlayerFactory}. + * SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder}. * *

        Player components

        * @@ -117,6 +124,162 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; */ public interface ExoPlayer extends Player { + /** + * A builder for {@link ExoPlayer} instances. + * + *

        See {@link #Builder(Context, Renderer...)} for the list of default values. + */ + final class Builder { + + private final Renderer[] renderers; + + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private Looper looper; + private boolean buildCalled; + + /** + * Creates a builder with a list of {@link Renderer Renderers}. + * + *

        The builder uses the following default values: + * + *

          + *
        • {@link RenderersFactory}: {@link DefaultRenderersFactory} + *
        • {@link TrackSelector}: {@link DefaultTrackSelector} + *
        • {@link LoadControl}: {@link DefaultLoadControl} + *
        • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} + *
        • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link + * Looper} of the application's main thread if the current thread doesn't have a {@link + * Looper} + *
        • {@link Clock}: {@link Clock#DEFAULT} + *
        + * + * @param context A {@link Context}. + * @param renderers The {@link Renderer Renderers} to be used by the player. + */ + public Builder(Context context, Renderer... renderers) { + this( + renderers, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + Clock.DEFAULT); + } + + /** + * Creates a builder with the specified custom components. + * + *

        Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param renderers The {@link Renderer Renderers} to be used by the player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper, + Clock clock) { + Assertions.checkArgument(renderers.length > 0); + this.renderers = renderers; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds an {@link ExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public ExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new ExoPlayerImpl( + renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + } + } + /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); 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 9168f1bd76..82bc94dab8 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 @@ -28,30 +28,15 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; -/** - * A factory for {@link ExoPlayer} instances. - */ +/** @deprecated Use {@link SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder} instead. */ +@Deprecated public final class ExoPlayerFactory { - private static @Nullable BandwidthMeter singletonBandwidthMeter; - private ExoPlayerFactory() {} - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, - * LoadControl, DrmSessionManager)}. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, @@ -64,23 +49,9 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to - * seamlessly join an ongoing playback. - * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, - * LoadControl, DrmSessionManager)}. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, @@ -96,59 +67,40 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance(Context context) { return newSimpleInstance(context, new DefaultTrackSelector(context)); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) { return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl()); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, LoadControl loadControl) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context); return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } - /** - * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, @@ -159,15 +111,9 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} 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. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -177,14 +123,9 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -199,16 +140,9 @@ public final class ExoPlayerFactory { Util.getLooper()); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -219,17 +153,9 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper()); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -248,18 +174,9 @@ public final class ExoPlayerFactory { Util.getLooper()); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 analyticsCollector The {@link AnalyticsCollector} that will collect and forward all - * player events. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -277,18 +194,9 @@ public final class ExoPlayerFactory { Util.getLooper()); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -306,20 +214,9 @@ public final class ExoPlayerFactory { looper); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 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. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -334,25 +231,13 @@ public final class ExoPlayerFactory { trackSelector, loadControl, drmSessionManager, - getDefaultBandwidthMeter(context), + DefaultBandwidthMeter.getSingletonInstance(context), analyticsCollector, looper); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 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. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -373,41 +258,25 @@ public final class ExoPlayerFactory { looper); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static ExoPlayer newInstance( Context context, Renderer[] renderers, TrackSelector trackSelector) { return newInstance(context, renderers, trackSelector, new DefaultLoadControl()); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static ExoPlayer newInstance( Context context, Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return newInstance(context, renderers, trackSelector, loadControl, Util.getLooper()); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static ExoPlayer newInstance( Context context, Renderer[] renderers, @@ -415,21 +284,16 @@ public final class ExoPlayerFactory { LoadControl loadControl, Looper looper) { return newInstance( - context, renderers, trackSelector, loadControl, getDefaultBandwidthMeter(context), looper); + context, + renderers, + trackSelector, + loadControl, + DefaultBandwidthMeter.getSingletonInstance(context), + looper); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ - @SuppressWarnings("unused") + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated public static ExoPlayer newInstance( Context context, Renderer[] renderers, @@ -440,11 +304,4 @@ public final class ExoPlayerFactory { return new ExoPlayerImpl( renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); } - - private static synchronized BandwidthMeter getDefaultBandwidthMeter(Context context) { - if (singletonBandwidthMeter == null) { - singletonBandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); - } - return singletonBandwidthMeter; - } } 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 e99429d3b2..cacdaec02e 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 @@ -37,7 +37,9 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; import java.util.concurrent.CopyOnWriteArrayList; -/** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */ +/** + * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}. + */ /* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer { private static final String TAG = "ExoPlayerImpl"; 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 8913fbdaba..749cf79e78 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 @@ -24,6 +24,7 @@ import android.media.PlaybackParams; import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -45,9 +46,11 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; @@ -63,7 +66,7 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can - * be obtained from {@link ExoPlayerFactory}. + * be obtained from {@link SimpleExoPlayer.Builder}. */ public class SimpleExoPlayer extends BasePlayer implements ExoPlayer, @@ -76,6 +79,229 @@ public class SimpleExoPlayer extends BasePlayer @Deprecated public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {} + /** + * A builder for {@link SimpleExoPlayer} instances. + * + *

        See {@link #Builder(Context)} for the list of default values. + */ + public static final class Builder { + + private final Context context; + private final RenderersFactory renderersFactory; + + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private DrmSessionManager drmSessionManager; + private BandwidthMeter bandwidthMeter; + private AnalyticsCollector analyticsCollector; + private Looper looper; + private boolean buildCalled; + + /** + * Creates a builder. + * + *

        Use {@link #Builder(Context, RenderersFactory)} instead, if you intend to provide a custom + * {@link RenderersFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link + * DefaultRenderersFactory} from the APK. + * + *

        The builder uses the following default values: + * + *

          + *
        • {@link RenderersFactory}: {@link DefaultRenderersFactory} + *
        • {@link TrackSelector}: {@link DefaultTrackSelector} + *
        • {@link LoadControl}: {@link DefaultLoadControl} + *
        • {@link DrmSessionManager}: {@link DrmSessionManager#getDummyDrmSessionManager()} + *
        • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} + *
        • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link + * Looper} of the application's main thread if the current thread doesn't have a {@link + * Looper} + *
        • {@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT} + *
        • {@link Clock}: {@link Clock#DEFAULT} + *
        + * + * @param context A {@link Context}. + */ + public Builder(Context context) { + this(context, new DefaultRenderersFactory(context)); + } + + /** + * Creates a builder with a custom {@link RenderersFactory}. + * + *

        See {@link #Builder(Context)} for a list of default values. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + */ + public Builder(Context context, RenderersFactory renderersFactory) { + this( + context, + renderersFactory, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DrmSessionManager.getDummyDrmSessionManager(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + Clock.DEFAULT); + } + + /** + * Creates a builder with the specified custom components. + * + *

        Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param drmSessionManager A {@link DrmSessionManager}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter, + Looper looper, + AnalyticsCollector analyticsCollector, + Clock clock) { + this.context = context; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.drmSessionManager = drmSessionManager; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link DrmSessionManager} that will be used for DRM protected playbacks. + * + * @param drmSessionManager A {@link DrmSessionManager}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!buildCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds a {@link SimpleExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public SimpleExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new SimpleExoPlayer( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + } + private static final String TAG = "SimpleExoPlayer"; protected final Renderer[] renderers; 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 c5d22c15cb..f457701031 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.trackselection; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; @@ -64,7 +63,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed - * to the player in {@link ExoPlayerFactory}. + * to the player in {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -112,7 +111,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should - * be directly passed to the player in {@link ExoPlayerFactory}. + * be directly passed to the player in {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -181,7 +180,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom - * bandwidth meter should be directly passed to the player in {@link ExoPlayerFactory}. + * bandwidth meter should be directly passed to the player in {@link + * SimpleExoPlayer.Builder}. */ @Deprecated public Factory( 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 8e1284f7ef..b43701f1bc 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 @@ -26,7 +26,6 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; @@ -1408,7 +1407,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be - * passed directly to the player in {@link ExoPlayerFactory}. + * passed directly to the player in {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") 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 9f76ca544f..67dd9a7a55 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 @@ -79,6 +79,8 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default maximum weight for the sliding window. */ public static final int DEFAULT_SLIDING_WINDOW_MAX_WEIGHT = 2000; + @Nullable private static DefaultBandwidthMeter singletonInstance; + /** Builder for a bandwidth meter. */ public static final class Builder { @@ -214,6 +216,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } } + /** + * Returns a singleton instance of a {@link DefaultBandwidthMeter} with default configuration. + * + * @param context A {@link Context}. + * @return The singleton instance. + */ + public static synchronized DefaultBandwidthMeter getSingletonInstance(Context context) { + if (singletonInstance == null) { + singletonInstance = new DefaultBandwidthMeter.Builder(context).build(); + } + return singletonInstance; + } + private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000; private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024; 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 e452e391d5..e2fc079887 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 @@ -23,8 +23,6 @@ import android.media.UnsupportedSchemeException; 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.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -290,12 +288,10 @@ public final class DashTestRunner { MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { SimpleExoPlayer player = - ExoPlayerFactory.newSimpleInstance( - host, - new DebugRenderersFactory(host), - trackSelector, - new DefaultLoadControl(), - drmSessionManager); + new SimpleExoPlayer.Builder(host, new DebugRenderersFactory(host)) + .setTrackSelector(trackSelector) + .setDrmSessionManager(drmSessionManager) + .build(); player.setVideoSurface(surface); return player; } 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 3ebd47b7a6..214c51c00c 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 @@ -22,13 +22,10 @@ import android.os.Looper; import android.os.SystemClock; import android.view.Surface; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.DefaultAudioSink; @@ -245,14 +242,14 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { Surface surface, MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { - RenderersFactory renderersFactory = - new DefaultRenderersFactory( - host, - DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, - /* allowedVideoJoiningTimeMs= */ 0); + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(host); + renderersFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); + renderersFactory.setAllowedVideoJoiningTimeMs(/* allowedVideoJoiningTimeMs= */ 0); SimpleExoPlayer player = - ExoPlayerFactory.newSimpleInstance( - host, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); + new SimpleExoPlayer.Builder(host, renderersFactory) + .setTrackSelector(trackSelector) + .setDrmSessionManager(drmSessionManager) + .build(); player.setVideoSurface(surface); return player; } From 76a6f5b0d05708f7872e798d2c94960715be6398 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Aug 2019 16:24:03 +0100 Subject: [PATCH 325/807] Remove RenderersFactory from javadoc The builder takes renderers instead of using a factory. PiperOrigin-RevId: 263354003 --- .../src/main/java/com/google/android/exoplayer2/ExoPlayer.java | 1 - 1 file changed, 1 deletion(-) 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 27c3a630f8..991be9b08b 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 @@ -146,7 +146,6 @@ public interface ExoPlayer extends Player { *

        The builder uses the following default values: * *

          - *
        • {@link RenderersFactory}: {@link DefaultRenderersFactory} *
        • {@link TrackSelector}: {@link DefaultTrackSelector} *
        • {@link LoadControl}: {@link DefaultLoadControl} *
        • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} From f3a1b099e6717c650966ff0b12e43c5ecbaa0605 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Aug 2019 16:37:26 +0100 Subject: [PATCH 326/807] Fix propagation of HlsMetadataEntry's in HLS chunkless preparation PiperOrigin-RevId: 263356275 --- .../android/exoplayer2/source/hls/HlsMediaPeriod.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 f4f91cf1b4..ad2a3ad265 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 @@ -25,6 +25,7 @@ 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.metadata.Metadata; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -787,7 +788,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, - /* metadata= */ null, + variantFormat.metadata, variantFormat.bitrate, variantFormat.width, variantFormat.height, @@ -800,6 +801,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private static Format deriveAudioFormat( Format variantFormat, Format mediaTagFormat, boolean isPrimaryTrackInVariant) { String codecs; + Metadata metadata; int channelCount = Format.NO_VALUE; int selectionFlags = 0; int roleFlags = 0; @@ -807,6 +809,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper String label = null; if (mediaTagFormat != null) { codecs = mediaTagFormat.codecs; + metadata = mediaTagFormat.metadata; channelCount = mediaTagFormat.channelCount; selectionFlags = mediaTagFormat.selectionFlags; roleFlags = mediaTagFormat.roleFlags; @@ -814,6 +817,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper label = mediaTagFormat.label; } else { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); + metadata = variantFormat.metadata; if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; @@ -830,7 +834,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper variantFormat.containerMimeType, sampleMimeType, codecs, - /* metadata= */ null, + metadata, bitrate, channelCount, /* sampleRate= */ Format.NO_VALUE, From bdc87908962649985d3b883d73ac6f9180d340ce Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Aug 2019 16:39:08 +0100 Subject: [PATCH 327/807] Remove experimental track bitrate estimator features. We are not planning to use them in the near future, so remove the experimental flags and related features. PiperOrigin-RevId: 263356590 --- .../AdaptiveTrackSelection.java | 56 +- .../trackselection/TrackBitrateEstimator.java | 53 -- .../trackselection/TrackSelectionUtil.java | 266 -------- .../WindowedTrackBitrateEstimator.java | 66 -- .../AdaptiveTrackSelectionTest.java | 53 -- .../TrackSelectionUtilTest.java | 617 ------------------ .../WindowedTrackBitrateEstimatorTest.java | 175 ----- 7 files changed, 5 insertions(+), 1281 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java 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 f457701031..fc3783d56b 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 @@ -47,8 +47,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long minTimeBetweenBufferReevaluationMs; private final Clock clock; - private TrackBitrateEstimator trackBitrateEstimator; - /** Creates an adaptive track selection factory with default parameters. */ public Factory() { this( @@ -202,19 +200,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { bufferedFractionToLiveEdgeForQualityIncrease; this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; this.clock = clock; - trackBitrateEstimator = TrackBitrateEstimator.DEFAULT; - } - - /** - * Sets a TrackBitrateEstimator. - * - *

          This method is experimental, and will be renamed or removed in a future release. - * - * @param trackBitrateEstimator A {@link TrackBitrateEstimator}. - */ - public final void experimental_setTrackBitrateEstimator( - TrackBitrateEstimator trackBitrateEstimator) { - this.trackBitrateEstimator = trackBitrateEstimator; } @Override @@ -245,7 +230,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { AdaptiveTrackSelection adaptiveSelection = createAdaptiveTrackSelection( definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); - adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); adaptiveSelections.add(adaptiveSelection); selections[i] = adaptiveSelection; } @@ -312,11 +296,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final float bufferedFractionToLiveEdgeForQualityIncrease; private final long minTimeBetweenBufferReevaluationMs; private final Clock clock; - private final Format[] formats; - private final int[] formatBitrates; - private final int[] trackBitrates; - private TrackBitrateEstimator trackBitrateEstimator; private float playbackSpeed; private int selectedIndex; private int reason; @@ -419,27 +399,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { playbackSpeed = 1f; reason = C.SELECTION_REASON_UNKNOWN; lastBufferEvaluationMs = C.TIME_UNSET; - trackBitrateEstimator = TrackBitrateEstimator.DEFAULT; - formats = new Format[length]; - formatBitrates = new int[length]; - trackBitrates = new int[length]; - for (int i = 0; i < length; i++) { - @SuppressWarnings("nullness:method.invocation.invalid") - Format format = getFormat(i); - formats[i] = format; - formatBitrates[i] = formats[i].bitrate; - } - } - - /** - * Sets a TrackBitrateEstimator. - * - *

          This method is experimental, and will be renamed or removed in a future release. - * - * @param trackBitrateEstimator A {@link TrackBitrateEstimator}. - */ - public void experimental_setTrackBitrateEstimator(TrackBitrateEstimator trackBitrateEstimator) { - this.trackBitrateEstimator = trackBitrateEstimator; } /** @@ -472,19 +431,16 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { MediaChunkIterator[] mediaChunkIterators) { long nowMs = clock.elapsedRealtime(); - // Update the estimated track bitrates. - trackBitrateEstimator.getBitrates(formats, queue, mediaChunkIterators, trackBitrates); - // Make initial selection if (reason == C.SELECTION_REASON_UNKNOWN) { reason = C.SELECTION_REASON_INITIAL; - selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates); + selectedIndex = determineIdealSelectedIndex(nowMs); return; } // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; - selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates); + selectedIndex = determineIdealSelectedIndex(nowMs); if (selectedIndex == currentSelectedIndex) { return; } @@ -548,7 +504,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) { return queueSize; } - int idealSelectedIndex = determineIdealSelectedIndex(nowMs, formatBitrates); + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); Format idealFormat = getFormat(idealSelectedIndex); // If the chunks contain video, discard from the first SD chunk beyond // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal @@ -613,16 +569,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link * Long#MIN_VALUE} to ignore blacklisting. - * @param trackBitrates The estimated track bitrates. May differ from format bitrates if more - * accurate estimates of the current track bitrates are available. */ - private int determineIdealSelectedIndex(long nowMs, int[] trackBitrates) { + private int determineIdealSelectedIndex(long nowMs) { long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth(); int lowestBitrateNonBlacklistedIndex = 0; for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { Format format = getFormat(i); - if (canSelectFormat(format, trackBitrates[i], playbackSpeed, effectiveBitrate)) { + if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) { return i; } else { lowestBitrateNonBlacklistedIndex = i; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java deleted file mode 100644 index 1cd6c09bfe..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java +++ /dev/null @@ -1,53 +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.trackselection; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import java.util.List; - -/** Estimates track bitrate values. */ -public interface TrackBitrateEstimator { - - /** - * A {@link TrackBitrateEstimator} that returns the bitrate values defined in the track formats. - */ - TrackBitrateEstimator DEFAULT = - (formats, queue, iterators, bitrates) -> - TrackSelectionUtil.getFormatBitrates(formats, bitrates); - - /** - * Returns bitrate values for a set of tracks whose formats are given. - * - * @param formats The track formats. - * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified. - * @param iterators An array of {@link MediaChunkIterator}s providing information about the - * sequence of upcoming media chunks for each track. - * @param bitrates An array into which the bitrate values will be written. If non-null, this array - * is the one that will be returned. - * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a - * bitrate value is set in the returned array. Otherwise it might be set to {@link - * Format#NO_VALUE}. - */ - int[] getBitrates( - Format[] formats, - List queue, - MediaChunkIterator[] iterators, - @Nullable int[] bitrates); -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java index 71afd87b0f..0f2748b1ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java @@ -16,18 +16,9 @@ package com.google.android.exoplayer2.trackselection; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.util.Assertions; -import java.util.Arrays; -import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; /** Track selection related utility methods. */ @@ -106,261 +97,4 @@ public final class TrackSelectionUtil { } return builder.build(); } - - /** - * Returns average bitrate for chunks in bits per second. Chunks are included in average until - * {@code maxDurationMs} or the first unknown length chunk. - * - * @param iterator Iterator for media chunk sequences. - * @param maxDurationUs Maximum duration of chunks to be included in average bitrate, in - * microseconds. - * @return Average bitrate for chunks in bits per second, or {@link Format#NO_VALUE} if there are - * no chunks or the first chunk length is unknown. - */ - public static int getAverageBitrate(MediaChunkIterator iterator, long maxDurationUs) { - long totalDurationUs = 0; - long totalLength = 0; - while (iterator.next()) { - long chunkLength = iterator.getDataSpec().length; - if (chunkLength == C.LENGTH_UNSET) { - break; - } - long chunkDurationUs = iterator.getChunkEndTimeUs() - iterator.getChunkStartTimeUs(); - if (totalDurationUs + chunkDurationUs >= maxDurationUs) { - totalLength += chunkLength * (maxDurationUs - totalDurationUs) / chunkDurationUs; - totalDurationUs = maxDurationUs; - break; - } - totalDurationUs += chunkDurationUs; - totalLength += chunkLength; - } - return totalDurationUs == 0 - ? Format.NO_VALUE - : (int) (totalLength * C.BITS_PER_BYTE * C.MICROS_PER_SECOND / totalDurationUs); - } - - /** - * Returns bitrate values for a set of tracks whose upcoming media chunk iterators and formats are - * given. - * - *

          If an average bitrate can't be calculated, an estimation is calculated using average bitrate - * of another track and the ratio of the bitrate values defined in the formats of the two tracks. - * - * @param iterators An array of {@link MediaChunkIterator}s providing information about the - * sequence of upcoming media chunks for each track. - * @param formats The track formats. - * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in - * microseconds. - * @param bitrates If not null, stores bitrate values in this array. - * @return Average bitrate values for the tracks. If for a track, an average bitrate or an - * estimation can't be calculated, {@link Format#NO_VALUE} is set. - * @see #getAverageBitrate(MediaChunkIterator, long) - */ - @VisibleForTesting - /* package */ static int[] getBitratesUsingFutureInfo( - MediaChunkIterator[] iterators, - Format[] formats, - long maxDurationUs, - @Nullable int[] bitrates) { - int trackCount = iterators.length; - Assertions.checkArgument(trackCount == formats.length); - if (trackCount == 0) { - return new int[0]; - } - if (bitrates == null) { - bitrates = new int[trackCount]; - } - if (maxDurationUs == 0) { - Arrays.fill(bitrates, Format.NO_VALUE); - return bitrates; - } - - int[] formatBitrates = new int[trackCount]; - float[] bitrateRatios = new float[trackCount]; - boolean needEstimateBitrate = false; - boolean canEstimateBitrate = false; - for (int i = 0; i < trackCount; i++) { - int bitrate = getAverageBitrate(iterators[i], maxDurationUs); - if (bitrate != Format.NO_VALUE) { - int formatBitrate = formats[i].bitrate; - formatBitrates[i] = formatBitrate; - if (formatBitrate != Format.NO_VALUE) { - bitrateRatios[i] = ((float) bitrate) / formatBitrate; - canEstimateBitrate = true; - } - } else { - needEstimateBitrate = true; - formatBitrates[i] = Format.NO_VALUE; - } - bitrates[i] = bitrate; - } - - if (needEstimateBitrate && canEstimateBitrate) { - estimateBitrates(bitrates, formats, formatBitrates, bitrateRatios); - } - return bitrates; - } - - /** - * Returns bitrate values for a set of tracks whose formats are given, using the given queue of - * already buffered {@link MediaChunk} instances. - * - * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified. - * @param formats The track formats. - * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in - * microseconds. - * @param bitrates If not null, calculates bitrate values only for indexes set to Format.NO_VALUE - * and stores result in this array. - * @return Bitrate values for the tracks. If for a track, a bitrate value can't be calculated, - * {@link Format#NO_VALUE} is set. - * @see #getBitratesUsingFutureInfo(MediaChunkIterator[], Format[], long, int[]) - */ - @VisibleForTesting - /* package */ static int[] getBitratesUsingPastInfo( - List queue, - Format[] formats, - long maxDurationUs, - @Nullable int[] bitrates) { - if (bitrates == null) { - bitrates = new int[formats.length]; - Arrays.fill(bitrates, Format.NO_VALUE); - } - if (maxDurationUs == 0) { - return bitrates; - } - int queueAverageBitrate = getAverageQueueBitrate(queue, maxDurationUs); - if (queueAverageBitrate == Format.NO_VALUE) { - return bitrates; - } - int queueFormatBitrate = queue.get(queue.size() - 1).trackFormat.bitrate; - if (queueFormatBitrate != Format.NO_VALUE) { - float queueBitrateRatio = ((float) queueAverageBitrate) / queueFormatBitrate; - estimateBitrates( - bitrates, formats, new int[] {queueFormatBitrate}, new float[] {queueBitrateRatio}); - } - return bitrates; - } - - /** - * Returns bitrate values for a set of tracks whose formats are given, using the given upcoming - * media chunk iterators and the queue of already buffered {@link MediaChunk}s. - * - * @param formats The track formats. - * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified. - * @param maxPastDurationUs Maximum duration of past chunks to be included in average bitrate - * values, in microseconds. - * @param iterators An array of {@link MediaChunkIterator}s providing information about the - * sequence of upcoming media chunks for each track. - * @param maxFutureDurationUs Maximum duration of future chunks to be included in average bitrate - * values, in microseconds. - * @param useFormatBitrateAsLowerBound Whether to return the estimated bitrate only if it's higher - * than the bitrate of the track's format. - * @param bitrates An array into which the bitrate values will be written. If non-null, this array - * is the one that will be returned. - * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a - * bitrate value is set in the returned array. Otherwise it might be set to {@link - * Format#NO_VALUE}. - */ - public static int[] getBitratesUsingPastAndFutureInfo( - Format[] formats, - List queue, - long maxPastDurationUs, - MediaChunkIterator[] iterators, - long maxFutureDurationUs, - boolean useFormatBitrateAsLowerBound, - @Nullable int[] bitrates) { - bitrates = getBitratesUsingFutureInfo(iterators, formats, maxFutureDurationUs, bitrates); - getBitratesUsingPastInfo(queue, formats, maxPastDurationUs, bitrates); - for (int i = 0; i < bitrates.length; i++) { - int bitrate = bitrates[i]; - if (bitrate == Format.NO_VALUE - || (useFormatBitrateAsLowerBound - && formats[i].bitrate != Format.NO_VALUE - && bitrate < formats[i].bitrate)) { - bitrates[i] = formats[i].bitrate; - } - } - return bitrates; - } - - /** - * Returns an array containing {@link Format#bitrate} values for given each format in order. - * - * @param formats The format array to copy {@link Format#bitrate} values. - * @param bitrates If not null, stores bitrate values in this array. - * @return An array containing {@link Format#bitrate} values for given each format in order. - */ - public static int[] getFormatBitrates(Format[] formats, @Nullable int[] bitrates) { - int trackCount = formats.length; - if (bitrates == null) { - bitrates = new int[trackCount]; - } - for (int i = 0; i < trackCount; i++) { - bitrates[i] = formats[i].bitrate; - } - return bitrates; - } - - /** - * Fills missing values in the given {@code bitrates} array by calculates an estimation using the - * closest reference bitrate value. - * - * @param bitrates An array of bitrates to be filled with estimations. Missing values are set to - * {@link Format#NO_VALUE}. - * @param formats An array of formats, one for each bitrate. - * @param referenceBitrates An array of reference bitrates which are used to calculate - * estimations. - * @param referenceBitrateRatios An array containing ratio of reference bitrates to their bitrate - * estimates. - */ - private static void estimateBitrates( - int[] bitrates, Format[] formats, int[] referenceBitrates, float[] referenceBitrateRatios) { - for (int i = 0; i < bitrates.length; i++) { - if (bitrates[i] == Format.NO_VALUE) { - int formatBitrate = formats[i].bitrate; - if (formatBitrate != Format.NO_VALUE) { - int closestReferenceBitrateIndex = - getClosestBitrateIndex(formatBitrate, referenceBitrates); - bitrates[i] = - (int) (referenceBitrateRatios[closestReferenceBitrateIndex] * formatBitrate); - } - } - } - } - - private static int getAverageQueueBitrate(List queue, long maxDurationUs) { - if (queue.isEmpty()) { - return Format.NO_VALUE; - } - MediaChunkListIterator iterator = - new MediaChunkListIterator(getSingleFormatSubQueue(queue), /* reverseOrder= */ true); - return getAverageBitrate(iterator, maxDurationUs); - } - - private static List getSingleFormatSubQueue( - List queue) { - Format queueFormat = queue.get(queue.size() - 1).trackFormat; - int queueSize = queue.size(); - for (int i = queueSize - 2; i >= 0; i--) { - if (!queue.get(i).trackFormat.equals(queueFormat)) { - return queue.subList(i + 1, queueSize); - } - } - return queue; - } - - private static int getClosestBitrateIndex(int formatBitrate, int[] formatBitrates) { - int closestDistance = Integer.MAX_VALUE; - int closestFormat = C.INDEX_UNSET; - for (int j = 0; j < formatBitrates.length; j++) { - if (formatBitrates[j] != Format.NO_VALUE) { - int distance = Math.abs(formatBitrates[j] - formatBitrate); - if (distance < closestDistance) { - closestDistance = distance; - closestFormat = j; - } - } - } - return closestFormat; - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java deleted file mode 100644 index 25f7e4ea73..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java +++ /dev/null @@ -1,66 +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.trackselection; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import java.util.List; - -/** A {@link TrackBitrateEstimator} which derives estimates from a window of time. */ -public final class WindowedTrackBitrateEstimator implements TrackBitrateEstimator { - - private final long maxPastDurationUs; - private final long maxFutureDurationUs; - private final boolean useFormatBitrateAsLowerBound; - - /** - * @param maxPastDurationMs Maximum duration of past chunks to be included in average bitrate - * values, in milliseconds. - * @param maxFutureDurationMs Maximum duration of future chunks to be included in average bitrate - * values, in milliseconds. - * @param useFormatBitrateAsLowerBound Whether to use the bitrate of the track's format as a lower - * bound for the estimated bitrate. - */ - public WindowedTrackBitrateEstimator( - long maxPastDurationMs, long maxFutureDurationMs, boolean useFormatBitrateAsLowerBound) { - this.maxPastDurationUs = C.msToUs(maxPastDurationMs); - this.maxFutureDurationUs = C.msToUs(maxFutureDurationMs); - this.useFormatBitrateAsLowerBound = useFormatBitrateAsLowerBound; - } - - @Override - public int[] getBitrates( - Format[] formats, - List queue, - MediaChunkIterator[] iterators, - @Nullable int[] bitrates) { - if (maxFutureDurationUs > 0 || maxPastDurationUs > 0) { - return TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - formats, - queue, - maxPastDurationUs, - iterators, - maxFutureDurationUs, - useFormatBitrateAsLowerBound, - bitrates); - } - return TrackSelectionUtil.getFormatBitrates(formats, bitrates); - } -} 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 456f7f7107..af935048e8 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,9 +16,6 @@ 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.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -37,13 +34,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentMatcher; import org.mockito.Mock; /** Unit test for {@link AdaptiveTrackSelection}. */ @@ -231,54 +226,6 @@ public final class AdaptiveTrackSelectionTest { assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); } - @Test - public void testUpdateSelectedTrackSwitchUpIfTrackBitrateEstimateIsLow() { - Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); - Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); - Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); - TrackGroup trackGroup = new TrackGroup(format1, format2, format3); - - // The second measurement onward returns 1500L, which isn't enough to switch up to format3 as - // the format bitrate is 2000. - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 1500L); - - // But TrackBitrateEstimator returns 1500 for 3rd track so it should switch up. - TrackBitrateEstimator estimator = mock(TrackBitrateEstimator.class); - when(estimator.getBitrates(any(), any(), any(), any())) - .then( - (invocation) -> { - int[] returnValue = new int[] {500, 1000, 1500}; - int[] inputArray = (int[]) invocation.getArguments()[3]; - System.arraycopy(returnValue, 0, inputArray, 0, returnValue.length); - return returnValue; - }); - - adaptiveTrackSelection = adaptiveTrackSelection(trackGroup); - adaptiveTrackSelection.experimental_setTrackBitrateEstimator(estimator); - - adaptiveTrackSelection.updateSelectedTrack( - /* playbackPositionUs= */ 0, - /* bufferedDurationUs= */ AdaptiveTrackSelection - .DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS - * 1000, - /* availableDurationUs= */ C.TIME_UNSET, - /* queue= */ Collections.emptyList(), - /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); - - ArgumentMatcher matcher = - formats -> - formats.length == 3 - && Arrays.asList(formats).containsAll(Arrays.asList(format1, format2, format3)); - verify(estimator) - .getBitrates( - argThat(matcher), - eq(Collections.emptyList()), - eq(THREE_EMPTY_MEDIA_CHUNK_ITERATORS), - any()); - assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - @Test public void testEvaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java deleted file mode 100644 index 963e90f139..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java +++ /dev/null @@ -1,617 +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.trackselection; - -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.Format; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.testutil.FakeMediaChunk; -import com.google.android.exoplayer2.testutil.FakeMediaChunkIterator; -import com.google.android.exoplayer2.upstream.DataSpec; -import java.util.Arrays; -import java.util.Collections; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** {@link TrackSelectionUtil} tests. */ -@RunWith(AndroidJUnit4.class) -public class TrackSelectionUtilTest { - - public static final long MAX_DURATION_US = 30 * C.MICROS_PER_SECOND; - - @Test - public void getAverageBitrate_emptyIterator_returnsNoValue() { - assertThat(TrackSelectionUtil.getAverageBitrate(MediaChunkIterator.EMPTY, MAX_DURATION_US)) - .isEqualTo(Format.NO_VALUE); - } - - @Test - public void getAverageBitrate_oneChunk_returnsChunkBitrate() { - long[] chunkTimeBoundariesSec = {12, 17}; - long[] chunkLengths = {10}; - - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16); - } - - @Test - public void getAverageBitrate_multipleSameDurationChunks_returnsAverageChunkBitrate() { - long[] chunkTimeBoundariesSec = {0, 5, 10}; - long[] chunkLengths = {10, 20}; - - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(24); - } - - @Test - public void getAverageBitrate_multipleDifferentDurationChunks_returnsAverageChunkBitrate() { - long[] chunkTimeBoundariesSec = {0, 5, 15, 30}; - long[] chunkLengths = {10, 20, 30}; - - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16); - } - - @Test - public void getAverageBitrate_firstChunkLengthUnset_returnsNoValue() { - long[] chunkTimeBoundariesSec = {0, 5, 15, 30}; - long[] chunkLengths = {C.LENGTH_UNSET, 20, 30}; - - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)) - .isEqualTo(Format.NO_VALUE); - } - - @Test - public void getAverageBitrate_secondChunkLengthUnset_returnsFirstChunkBitrate() { - long[] chunkTimeBoundariesSec = {0, 5, 15, 30}; - long[] chunkLengths = {10, C.LENGTH_UNSET, 30}; - - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16); - } - - @Test - public void - getAverageBitrate_chunksExceedingMaxDuration_returnsAverageChunkBitrateUpToMaxDuration() { - long[] chunkTimeBoundariesSec = {0, 5, 15, 45, 50}; - long[] chunkLengths = {10, 20, 30, 100}; - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - long maxDurationUs = 30 * C.MICROS_PER_SECOND; - int averageBitrate = TrackSelectionUtil.getAverageBitrate(iterator, maxDurationUs); - - assertThat(averageBitrate).isEqualTo(12); - } - - @Test - public void getAverageBitrate_zeroMaxDuration_returnsNoValue() { - long[] chunkTimeBoundariesSec = {0, 5, 10}; - long[] chunkLengths = {10, 20}; - - FakeMediaChunkIterator iterator = - new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); - - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, /* maxDurationUs= */ 0)) - .isEqualTo(Format.NO_VALUE); - } - - @Test - public void getBitratesUsingFutureInfo_noIterator_returnsEmptyArray() { - assertThat( - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[0], new Format[0], MAX_DURATION_US, /* bitrates= */ null)) - .hasLength(0); - } - - @Test - public void getBitratesUsingFutureInfo_emptyIterator_returnsNoValue() { - int[] bitrates = - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[] {MediaChunkIterator.EMPTY}, - new Format[] {createFormatWithBitrate(10)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); - } - - @Test - public void getBitratesUsingFutureInfo_twoTracksZeroMaxDuration_returnsNoValue() { - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeMediaChunkIterator iterator2 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, - /* chunkLengths= */ new long[] {10, 20, 30}); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2}, - new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - /* maxDurationUs= */ 0, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE, Format.NO_VALUE); - } - - @Test - public void getBitratesUsingFutureInfo_twoTracks_returnsBitrates() { - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeMediaChunkIterator iterator2 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, - /* chunkLengths= */ new long[] {10, 20, 30}); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2}, - new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); - } - - @Test - public void getBitratesUsingFutureInfo_bitratesArrayGiven_returnsTheSameArray() { - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeMediaChunkIterator iterator2 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, - /* chunkLengths= */ new long[] {10, 20, 30}); - - int[] bitratesArrayToUse = new int[2]; - int[] bitrates = - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2}, - new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - MAX_DURATION_US, - bitratesArrayToUse); - - assertThat(bitrates).isSameInstanceAs(bitratesArrayToUse); - } - - @Test - public void getBitratesUsingFutureInfo_emptyIterator_returnsEstimationUsingClosest() { - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10}); - Format format1 = createFormatWithBitrate(10); - MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY; - Format format2 = createFormatWithBitrate(20); - FakeMediaChunkIterator iterator3 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {50}); - Format format3 = createFormatWithBitrate(25); - FakeMediaChunkIterator iterator4 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {20}); - Format format4 = createFormatWithBitrate(30); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2, iterator3, iterator4}, - new Format[] {format1, format2, format3, format4}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16, 64, 80, 32).inOrder(); - } - - @Test - public void getBitratesUsingFutureInfo_formatWithoutBitrate_returnsNoValueForEmpty() { - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10}); - Format format1 = createFormatWithBitrate(10); - MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY; - Format format2 = createFormatWithBitrate(Format.NO_VALUE); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2}, - new Format[] {format1, format2}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16, Format.NO_VALUE).inOrder(); - } - - @Test - public void getBitratesUsingPastInfo_noFormat_returnsEmptyArray() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), new Format[0], MAX_DURATION_US, /* bitrates= */ null); - - assertThat(bitrates).hasLength(0); - } - - @Test - public void getBitratesUsingPastInfo_emptyQueue_returnsNoValue() { - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.emptyList(), - new Format[] {createFormatWithBitrate(10)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); - } - - @Test - public void getBitratesUsingPastInfo_oneChunkFormatNoBitrate_returnsNoValue() { - Format format = createFormatWithBitrate(Format.NO_VALUE); - FakeMediaChunk chunk = - createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {format}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); - } - - @Test - public void getBitratesUsingPastInfo_oneChunkNoLength_returnsNoValue() { - Format format = createFormatWithBitrate(10); - FakeMediaChunk chunk = - createChunk( - format, /* length= */ C.LENGTH_UNSET, /* startTimeSec= */ 0, /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {format}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); - } - - @Test - public void getBitratesUsingPastInfo_oneChunkWithSameFormat_returnsBitrates() { - Format format = createFormatWithBitrate(10); - FakeMediaChunk chunk = - createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {format}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(8).inOrder(); - } - - @Test - public void getBitratesUsingPastInfo_zeroMaxDuration_returnsNoValue() { - Format format = createFormatWithBitrate(10); - FakeMediaChunk chunk = - createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {format}, - /* maxDurationUs= */ 0, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE).inOrder(); - } - - @Test - public void getBitratesUsingPastInfo_multipleChunkWithSameFormat_returnsAverageBitrate() { - Format format = createFormatWithBitrate(10); - FakeMediaChunk chunk = - createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); - FakeMediaChunk chunk2 = - createChunk(format, /* length= */ 20, /* startTimeSec= */ 10, /* endTimeSec= */ 20); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Arrays.asList(chunk, chunk2), - new Format[] {format}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(12).inOrder(); - } - - @Test - public void getBitratesUsingPastInfo_oneChunkWithDifferentFormat_returnsEstimationBitrate() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {createFormatWithBitrate(20)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16).inOrder(); - } - - @Test - public void getBitratesUsingPastInfo_trackFormatNoBitrate_returnsNoValue() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {createFormatWithBitrate(Format.NO_VALUE)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); - } - - @Test - public void getBitratesUsingPastInfo_multipleTracks_returnsBitrates() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16, 24).inOrder(); - } - - @Test - public void getBitratesUsingPastInfo_bitratesArrayGiven_returnsTheSameArray() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitratesArrayToUse = new int[2]; - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), - new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, - MAX_DURATION_US, - bitratesArrayToUse); - - assertThat(bitrates).isSameInstanceAs(bitratesArrayToUse); - } - - @Test - public void - getBitratesUsingPastInfo_multipleChunkExceedingMaxDuration_returnsAverageUntilMaxDuration() { - Format format = createFormatWithBitrate(10); - FakeMediaChunk chunk = - createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 20); - FakeMediaChunk chunk2 = - createChunk(format, /* length= */ 40, /* startTimeSec= */ 20, /* endTimeSec= */ 40); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Arrays.asList(chunk, chunk2), - new Format[] {format}, - /* maxDurationUs= */ 30 * C.MICROS_PER_SECOND, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(12).inOrder(); - } - - @Test - public void - getBitratesUsingPastInfo_chunksWithDifferentFormats_returnsChunkAverageBitrateForLastFormat() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - FakeMediaChunk chunk2 = - createChunk( - createFormatWithBitrate(20), - /* length= */ 40, - /* startTimeSec= */ 10, - /* endTimeSec= */ 20); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastInfo( - Arrays.asList(chunk, chunk2), - new Format[] {createFormatWithBitrate(10)}, - MAX_DURATION_US, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16).inOrder(); - } - - @Test - public void getBitratesUsingPastAndFutureInfo_noPastInfo_returnsBitratesUsingOnlyFutureInfo() { - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeMediaChunkIterator iterator2 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, - /* chunkLengths= */ new long[] {10, 20, 30}); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - Collections.emptyList(), - MAX_DURATION_US, - new MediaChunkIterator[] {iterator1, iterator2}, - MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); - } - - @Test - public void getBitratesUsingPastAndFutureInfo_noFutureInfo_returnsBitratesUsingOnlyPastInfo() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, - Collections.singletonList(chunk), - MAX_DURATION_US, - new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, - MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16, 24).inOrder(); - } - - @Test - public void - getBitratesUsingPastAndFutureInfo_pastAndFutureInfo_returnsBitratesUsingOnlyFutureInfo() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(5), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - FakeMediaChunkIterator iterator1 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeMediaChunkIterator iterator2 = - new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, - /* chunkLengths= */ new long[] {10, 20, 30}); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - Collections.singletonList(chunk), - MAX_DURATION_US, - new MediaChunkIterator[] {iterator1, iterator2}, - MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); - } - - @Test - public void getBitratesUsingPastAndFutureInfo_noPastAndFutureInfo_returnsBitratesOfFormats() { - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - Collections.emptyList(), - MAX_DURATION_US, - new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, - MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(10, 20).inOrder(); - } - - @Test - public void - getBitratesUsingPastAndFutureInfo_estimatesLowerAndUseFormatBitrateAsLowerBoundTrue_returnsBitratesOfFormats() { - FakeMediaChunk chunk = - createChunk( - createFormatWithBitrate(10), - /* length= */ 10, - /* startTimeSec= */ 0, - /* endTimeSec= */ 10); - - int[] bitrates = - TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, - Collections.singletonList(chunk), - MAX_DURATION_US, - new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, - MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ true, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(20, 30).inOrder(); - } - - private static FakeMediaChunk createChunk( - Format format, int length, int startTimeSec, int endTimeSec) { - DataSpec dataSpec = - new DataSpec( - Uri.EMPTY, /* absoluteStreamPosition= */ 0, length, /* key= */ null, /* flags= */ 0); - return new FakeMediaChunk( - dataSpec, format, startTimeSec * C.MICROS_PER_SECOND, endTimeSec * C.MICROS_PER_SECOND); - } - - private static Format createFormatWithBitrate(int bitrate) { - return Format.createSampleFormat( - /* id= */ null, - /* sampleMimeType= */ null, - /* codecs= */ null, - bitrate, - /* drmInitData= */ null); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java deleted file mode 100644 index d40149baae..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java +++ /dev/null @@ -1,175 +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.trackselection; - -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.Format; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.testutil.FakeMediaChunk; -import com.google.android.exoplayer2.testutil.FakeMediaChunkIterator; -import com.google.android.exoplayer2.upstream.DataSpec; -import java.util.Collections; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** {@link WindowedTrackBitrateEstimator} tests. */ -@RunWith(AndroidJUnit4.class) -public class WindowedTrackBitrateEstimatorTest { - - private static final long MAX_DURATION_MS = 30_000; - - @Test - public void getBitrates_zeroMaxDuration_returnsFormatBitrates() { - WindowedTrackBitrateEstimator estimator = - new WindowedTrackBitrateEstimator( - /* maxPastDurationMs= */ 0, - /* maxFutureDurationMs= */ 0, - /* useFormatBitrateAsLowerBound= */ false); - MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); - MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); - MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); - Format format1 = createFormatWithBitrate(10); - Format format2 = createFormatWithBitrate(20); - - int[] bitrates = - estimator.getBitrates( - new Format[] {format1, format2}, - Collections.singletonList(chunk), - new MediaChunkIterator[] {iterator1, iterator2}, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(10, 20).inOrder(); - } - - @Test - public void getBitrates_futureMaxDurationSet_returnsEstimateUsingFutureChunks() { - WindowedTrackBitrateEstimator estimator = - new WindowedTrackBitrateEstimator( - /* maxPastDurationMs= */ 0, MAX_DURATION_MS, /* useFormatBitrateAsLowerBound= */ false); - MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); - MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); - MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); - Format format1 = createFormatWithBitrate(10); - Format format2 = createFormatWithBitrate(20); - - int[] bitrates = - estimator.getBitrates( - new Format[] {format1, format2}, - Collections.singletonList(chunk), - new MediaChunkIterator[] {iterator1, iterator2}, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); - } - - @Test - public void getBitrates_pastMaxDurationSet_returnsEstimateUsingPastChunks() { - WindowedTrackBitrateEstimator estimator = - new WindowedTrackBitrateEstimator( - MAX_DURATION_MS, - /* maxFutureDurationMs= */ 0, - /* useFormatBitrateAsLowerBound= */ false); - MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); - MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); - MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); - Format format1 = createFormatWithBitrate(10); - Format format2 = createFormatWithBitrate(20); - - int[] bitrates = - estimator.getBitrates( - new Format[] {format1, format2}, - Collections.singletonList(chunk), - new MediaChunkIterator[] {iterator1, iterator2}, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(16, 32).inOrder(); - } - - @Test - public void - getBitrates_useFormatBitrateAsLowerBoundSetTrue_returnsEstimateIfOnlyHigherThanFormat() { - WindowedTrackBitrateEstimator estimator = - new WindowedTrackBitrateEstimator( - MAX_DURATION_MS, MAX_DURATION_MS, /* useFormatBitrateAsLowerBound= */ true); - MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); - MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(80); - MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); - Format format1 = createFormatWithBitrate(10); - Format format2 = createFormatWithBitrate(20); - - int[] bitrates = - estimator.getBitrates( - new Format[] {format1, format2}, - Collections.singletonList(chunk), - new MediaChunkIterator[] {iterator1, iterator2}, - /* bitrates= */ null); - - assertThat(bitrates).asList().containsExactly(80, 20).inOrder(); - } - - @Test - public void getBitrates_bitratesArrayGiven_returnsTheSameArray() { - WindowedTrackBitrateEstimator estimator = - new WindowedTrackBitrateEstimator( - MAX_DURATION_MS, MAX_DURATION_MS, /* useFormatBitrateAsLowerBound= */ true); - MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); - MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); - MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); - Format format1 = createFormatWithBitrate(10); - Format format2 = createFormatWithBitrate(20); - - int[] bitratesArrayToUse = new int[2]; - int[] bitrates = - estimator.getBitrates( - new Format[] {format1, format2}, - Collections.singletonList(chunk), - new MediaChunkIterator[] {iterator1, iterator2}, - bitratesArrayToUse); - - assertThat(bitrates).isSameInstanceAs(bitratesArrayToUse); - } - - private static MediaChunk createMediaChunk(int formatBitrate, int actualBitrate) { - int length = actualBitrate / C.BITS_PER_BYTE; - DataSpec dataSpec = - new DataSpec( - Uri.EMPTY, /* absoluteStreamPosition= */ 0, length, /* key= */ null, /* flags= */ 0); - Format format = createFormatWithBitrate(formatBitrate); - return new FakeMediaChunk( - dataSpec, format, /* startTimeUs= */ 0L, /* endTimeUs= */ C.MICROS_PER_SECOND); - } - - private static Format createFormatWithBitrate(int bitrate) { - return Format.createSampleFormat( - /* id= */ null, - /* sampleMimeType= */ null, - /* codecs= */ null, - bitrate, - /* drmInitData= */ null); - } - - private static MediaChunkIterator createMediaChunkIteratorWithBitrate(int bitrate) { - return new FakeMediaChunkIterator( - /* chunkTimeBoundariesSec= */ new long[] {0, 1}, - /* chunkLengths= */ new long[] {bitrate / C.BITS_PER_BYTE}); - } -} From 4b75d3338e01b809c4ac7928deef9e572153203f Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 14 Aug 2019 18:46:12 +0100 Subject: [PATCH 328/807] Expand FakeSampleStream to allow specifying a single sample I removed the buffer.flip() call because it seems incompatible with the way MetadataRenderer deals with the Stream - it calls flip() itself on line 126. Tests fail with flip() here, and pass without it... PiperOrigin-RevId: 263381799 --- .../exoplayer2/testutil/FakeSampleStream.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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 02d0e372e8..8b05b27046 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 @@ -32,6 +32,7 @@ public final class FakeSampleStream implements SampleStream { private final Format format; @Nullable private final EventDispatcher eventDispatcher; + private final byte[] sampleData; private boolean notifiedDownstreamFormat; private boolean readFormat; @@ -47,9 +48,23 @@ public final class FakeSampleStream implements SampleStream { */ public FakeSampleStream( Format format, @Nullable EventDispatcher eventDispatcher, boolean shouldOutputSample) { + this(format, eventDispatcher, new byte[] {0}); + readSample = !shouldOutputSample; + } + + /** + * Creates fake sample stream which outputs the given {@link Format}, one sample with the provided + * bytes, then end of stream. + * + * @param format The {@link Format} to output. + * @param eventDispatcher An {@link EventDispatcher} to notify of read events. + * @param sampleData The sample data to output. + */ + public FakeSampleStream( + Format format, @Nullable EventDispatcher eventDispatcher, byte[] sampleData) { this.format = format; this.eventDispatcher = eventDispatcher; - readSample = !shouldOutputSample; + this.sampleData = sampleData; } @Override @@ -58,8 +73,8 @@ public final class FakeSampleStream implements SampleStream { } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { if (eventDispatcher != null && !notifiedDownstreamFormat) { eventDispatcher.downstreamFormatChanged( C.TRACK_TYPE_UNKNOWN, @@ -75,9 +90,8 @@ public final class FakeSampleStream implements SampleStream { return C.RESULT_FORMAT_READ; } else if (!readSample) { buffer.timeUs = 0; - buffer.ensureSpaceForWrite(1); - buffer.data.put((byte) 0); - buffer.flip(); + buffer.ensureSpaceForWrite(sampleData.length); + buffer.data.put(sampleData); readSample = true; return C.RESULT_BUFFER_READ; } else { @@ -95,5 +109,4 @@ public final class FakeSampleStream implements SampleStream { public int skipData(long positionUs) { return 0; } - } From 6a122f4740ef5da4e23445309a20762da2f95639 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 15 Aug 2019 11:38:05 +0100 Subject: [PATCH 329/807] Document injection of DrmSessionManagers into MediaSources instead of Renderers PiperOrigin-RevId: 263532499 --- RELEASENOTES.md | 2 + .../android/exoplayer2/ExoPlayerFactory.java | 61 ++++++++++++++--- .../android/exoplayer2/SimpleExoPlayer.java | 67 +++++++++++++------ .../playbacktests/gts/DashTestRunner.java | 14 ++-- .../exoplayer2/testutil/ExoHostedTest.java | 17 ++--- 5 files changed, 113 insertions(+), 48 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bf89c83724..d166fb41c6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,8 @@ quality video can be loaded up to the full default buffer duration. * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and `ExoPlayer.Builder`. +* Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` + ([#5619](https://github.com/google/ExoPlayer/issues/5619)). ### 2.10.4 ### 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 82bc94dab8..ae5071717d 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 @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; 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.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -34,7 +35,11 @@ public final class ExoPlayerFactory { private ExoPlayerFactory() {} - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -49,7 +54,11 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -98,7 +107,11 @@ public final class ExoPlayerFactory { return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -111,7 +124,11 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -140,7 +157,11 @@ public final class ExoPlayerFactory { Util.getLooper()); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -153,7 +174,11 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper()); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -174,7 +199,11 @@ public final class ExoPlayerFactory { Util.getLooper()); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -194,7 +223,11 @@ public final class ExoPlayerFactory { Util.getLooper()); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -214,7 +247,11 @@ public final class ExoPlayerFactory { looper); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( @@ -236,7 +273,11 @@ public final class ExoPlayerFactory { looper); } - /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ @Deprecated public static SimpleExoPlayer newSimpleInstance( Context context, 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 749cf79e78..9606cc0d48 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 @@ -92,7 +92,6 @@ public class SimpleExoPlayer extends BasePlayer private Clock clock; private TrackSelector trackSelector; private LoadControl loadControl; - private DrmSessionManager drmSessionManager; private BandwidthMeter bandwidthMeter; private AnalyticsCollector analyticsCollector; private Looper looper; @@ -111,7 +110,6 @@ public class SimpleExoPlayer extends BasePlayer *

        • {@link RenderersFactory}: {@link DefaultRenderersFactory} *
        • {@link TrackSelector}: {@link DefaultTrackSelector} *
        • {@link LoadControl}: {@link DefaultLoadControl} - *
        • {@link DrmSessionManager}: {@link DrmSessionManager#getDummyDrmSessionManager()} *
        • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} *
        • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link * Looper} of the application's main thread if the current thread doesn't have a {@link @@ -141,7 +139,6 @@ public class SimpleExoPlayer extends BasePlayer renderersFactory, new DefaultTrackSelector(context), new DefaultLoadControl(), - DrmSessionManager.getDummyDrmSessionManager(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), new AnalyticsCollector(Clock.DEFAULT), @@ -160,7 +157,6 @@ public class SimpleExoPlayer extends BasePlayer * player. * @param trackSelector A {@link TrackSelector}. * @param loadControl A {@link LoadControl}. - * @param drmSessionManager A {@link DrmSessionManager}. * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. * @param analyticsCollector An {@link AnalyticsCollector}. @@ -171,7 +167,6 @@ public class SimpleExoPlayer extends BasePlayer RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, Looper looper, AnalyticsCollector analyticsCollector, @@ -180,7 +175,6 @@ public class SimpleExoPlayer extends BasePlayer this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; this.loadControl = loadControl; - this.drmSessionManager = drmSessionManager; this.bandwidthMeter = bandwidthMeter; this.looper = looper; this.analyticsCollector = analyticsCollector; @@ -213,19 +207,6 @@ public class SimpleExoPlayer extends BasePlayer return this; } - /** - * Sets the {@link DrmSessionManager} that will be used for DRM protected playbacks. - * - * @param drmSessionManager A {@link DrmSessionManager}. - * @return This builder. - * @throws IllegalStateException If {@link #build()} has already been called. - */ - public Builder setDrmSessionManager(DrmSessionManager drmSessionManager) { - Assertions.checkState(!buildCalled); - this.drmSessionManager = drmSessionManager; - return this; - } - /** * Sets the {@link BandwidthMeter} that will be used by the player. * @@ -294,7 +275,6 @@ public class SimpleExoPlayer extends BasePlayer renderersFactory, trackSelector, loadControl, - drmSessionManager, bandwidthMeter, analyticsCollector, clock, @@ -354,7 +334,11 @@ public class SimpleExoPlayer extends BasePlayer * will not be used for DRM protected playbacks. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * DrmSessionManager} to the {@link MediaSource} factories. */ + @Deprecated protected SimpleExoPlayer( Context context, RenderersFactory renderersFactory, @@ -386,7 +370,11 @@ public class SimpleExoPlayer extends BasePlayer * 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. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * DrmSessionManager} to the {@link MediaSource} factories. */ + @Deprecated protected SimpleExoPlayer( Context context, RenderersFactory renderersFactory, @@ -408,6 +396,41 @@ public class SimpleExoPlayer extends BasePlayer looper); } + /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector A factory for creating 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 + * used to call listeners on. + */ + @SuppressWarnings("deprecation") + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Clock clock, + Looper looper) { + this( + context, + renderersFactory, + trackSelector, + loadControl, + DrmSessionManager.getDummyDrmSessionManager(), + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + /** * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. @@ -422,7 +445,11 @@ public class SimpleExoPlayer extends BasePlayer * 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 * used to call listeners on. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * DrmSessionManager} to the {@link MediaSource} factories. */ + @Deprecated protected SimpleExoPlayer( Context context, RenderersFactory renderersFactory, 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 e2fc079887..0d966c9080 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 @@ -257,10 +257,10 @@ public final class DashTestRunner { } @Override - protected DefaultDrmSessionManager buildDrmSessionManager( + protected DrmSessionManager buildDrmSessionManager( final String userAgent) { if (widevineLicenseUrl == null) { - return null; + return DrmSessionManager.getDummyDrmSessionManager(); } try { MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, @@ -283,27 +283,25 @@ public final class DashTestRunner { @Override protected SimpleExoPlayer buildExoPlayer( - HostActivity host, - Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { + HostActivity host, Surface surface, MappingTrackSelector trackSelector) { SimpleExoPlayer player = new SimpleExoPlayer.Builder(host, new DebugRenderersFactory(host)) .setTrackSelector(trackSelector) - .setDrmSessionManager(drmSessionManager) .build(); player.setVideoSurface(surface); return player; } @Override - protected MediaSource buildSource(HostActivity host, String userAgent) { + protected MediaSource buildSource( + HostActivity host, String userAgent, DrmSessionManager drmSessionManager) { DataSource.Factory dataSourceFactory = this.dataSourceFactory != null ? this.dataSourceFactory : new DefaultDataSourceFactory(host, userAgent); Uri manifestUri = Uri.parse(manifestUrl); return new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MIN_LOADABLE_RETRY_COUNT)) .createMediaSource(manifestUri); } 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 214c51c00c..5f01d7724b 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 @@ -130,8 +130,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { // Build the player. trackSelector = buildTrackSelector(host); String userAgent = "ExoPlayerPlaybackTests"; - DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); - player = buildExoPlayer(host, surface, trackSelector, drmSessionManager); + player = buildExoPlayer(host, surface, trackSelector); player.setPlayWhenReady(true); player.addAnalyticsListener(this); player.addAnalyticsListener(new EventLogger(trackSelector, tag)); @@ -141,7 +140,8 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); pendingSchedule = null; } - player.prepare(buildSource(host, Util.getUserAgent(host, userAgent))); + DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); + player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); } @Override @@ -230,7 +230,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { protected DrmSessionManager buildDrmSessionManager(String userAgent) { // Do nothing. Interested subclasses may override. - return null; + return DrmSessionManager.getDummyDrmSessionManager(); } protected DefaultTrackSelector buildTrackSelector(HostActivity host) { @@ -238,23 +238,20 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { } protected SimpleExoPlayer buildExoPlayer( - HostActivity host, - Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { + HostActivity host, Surface surface, MappingTrackSelector trackSelector) { DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(host); renderersFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); renderersFactory.setAllowedVideoJoiningTimeMs(/* allowedVideoJoiningTimeMs= */ 0); SimpleExoPlayer player = new SimpleExoPlayer.Builder(host, renderersFactory) .setTrackSelector(trackSelector) - .setDrmSessionManager(drmSessionManager) .build(); player.setVideoSurface(surface); return player; } - protected abstract MediaSource buildSource(HostActivity host, String userAgent); + protected abstract MediaSource buildSource( + HostActivity host, String userAgent, DrmSessionManager drmSessionManager); protected void onPlayerErrorInternal(ExoPlaybackException error) { // Do nothing. Interested subclasses may override. From 567d078e9e0082a721751588607868d74a9c528c Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Thu, 15 Aug 2019 12:00:30 +0100 Subject: [PATCH 330/807] Fix createDecoder method declaration PiperOrigin-RevId: 263534628 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 3 ++- .../android/exoplayer2/video/SimpleDecoderVideoRenderer.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 aea422176e..dd4077964b 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 @@ -228,7 +228,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { VideoDecoderInputBuffer, ? extends VideoDecoderOutputBuffer, ? extends VideoDecoderException> - createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws VideoDecoderException { + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws VideoDecoderException { TraceUtil.beginSection("createVpxDecoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 3d18242466..ad59d11c4a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -544,7 +544,8 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { VideoDecoderInputBuffer, ? extends VideoDecoderOutputBuffer, ? extends VideoDecoderException> - createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws VideoDecoderException; + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws VideoDecoderException; /** * Dequeues output buffer. From ebb72e358f8bc6dd3fa39d3e7bce46269641074c Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 15 Aug 2019 12:09:48 +0100 Subject: [PATCH 331/807] Support unwrapping nested Metadata messages in MetadataRenderer Initially this supports ID3-in-EMSG, but can also be used to support SCTE35-in-EMSG too. PiperOrigin-RevId: 263535925 --- .../android/exoplayer2/metadata/Metadata.java | 26 ++- .../exoplayer2/metadata/MetadataRenderer.java | 46 +++++- .../metadata/emsg/EventMessage.java | 22 +++ .../metadata/MetadataRendererTest.java | 153 ++++++++++++++++++ 4 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index dbc1114bd5..35702da576 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; @@ -28,10 +29,27 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ public final class Metadata implements Parcelable { - /** - * A metadata entry. - */ - public interface Entry extends Parcelable {} + /** A metadata entry. */ + public interface Entry extends Parcelable { + + /** + * Returns the {@link Format} that can be used to decode the wrapped metadata in {@link + * #getWrappedMetadataBytes()}, or null if this Entry doesn't contain wrapped metadata. + */ + @Nullable + default Format getWrappedMetadataFormat() { + return null; + } + + /** + * Returns the bytes of the wrapped metadata in this Entry, or null if it doesn't contain + * wrapped metadata. + */ + @Nullable + default byte[] getWrappedMetadataBytes() { + return null; + } + } private final Entry[] entries; 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 0fc0a85104..0dc0dc6096 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 @@ -27,7 +27,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * A renderer for metadata. @@ -123,12 +125,18 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; Metadata metadata = decoder.decode(buffer); if (metadata != null) { - pendingMetadata[index] = metadata; - pendingMetadataTimestamps[index] = buffer.timeUs; - pendingMetadataCount++; + List entries = new ArrayList<>(metadata.length()); + decodeWrappedMetadata(metadata, entries); + if (!entries.isEmpty()) { + Metadata expandedMetadata = new Metadata(entries); + int index = + (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = expandedMetadata; + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; + } } } } else if (result == C.RESULT_FORMAT_READ) { @@ -144,6 +152,36 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } + /** + * Iterates through {@code metadata.entries} and checks each one to see if contains wrapped + * metadata. If it does, then we recursively decode the wrapped metadata. If it doesn't (recursion + * base-case), we add the {@link Metadata.Entry} to {@code decodedEntries} (output parameter). + */ + private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { + for (int i = 0; i < metadata.length(); i++) { + Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { + MetadataDecoder wrappedMetadataDecoder = + decoderFactory.createDecoder(wrappedMetadataFormat); + // wrappedMetadataFormat != null so wrappedMetadataBytes must be non-null too. + byte[] wrappedMetadataBytes = + Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); + buffer.clear(); + buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); + buffer.data.put(wrappedMetadataBytes); + buffer.flip(); + @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); + if (innerMetadata != null) { + // The decoding succeeded, so we'll try another level of unwrapping. + decodeWrappedMetadata(innerMetadata, decodedEntries); + } + } else { + // Entry doesn't contain any wrapped metadata, so output it directly. + decodedEntries.add(metadata.get(i)); + } + } + } + @Override protected void onDisabled() { flushPendingMetadata(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index ca1e390181..c9e9d54093 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -20,7 +20,10 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -29,6 +32,13 @@ import java.util.Arrays; */ public final class EventMessage implements Metadata.Entry { + @VisibleForTesting + public static final String ID3_SCHEME_ID = "https://developer.apple.com/streaming/emsg-id3"; + + private static final Format ID3_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + /** * The message scheme. */ @@ -81,6 +91,18 @@ public final class EventMessage implements Metadata.Entry { messageData = castNonNull(in.createByteArray()); } + @Override + @Nullable + public Format getWrappedMetadataFormat() { + return ID3_SCHEME_ID.equals(schemeIdUri) ? ID3_FORMAT : null; + } + + @Override + @Nullable + public byte[] getWrappedMetadataBytes() { + return ID3_SCHEME_ID.equals(schemeIdUri) ? messageData : null; + } + @Override public int hashCode() { if (hashCode == 0) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java new file mode 100644 index 0000000000..4de8bb76cc --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -0,0 +1,153 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MetadataRenderer}. */ +@RunWith(AndroidJUnit4.class) +public class MetadataRendererTest { + + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + + private final EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); + + @Test + public void decodeMetadata() throws Exception { + EventMessage emsg = + new EventMessage( + "urn:test-scheme-id", + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + "Test data".getBytes(UTF_8)); + + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + + assertThat(metadata).hasSize(1); + assertThat(metadata.get(0).length()).isEqualTo(1); + assertThat(metadata.get(0).get(0)).isEqualTo(emsg); + } + + @Test + public void decodeMetadata_handlesWrappedMetadata() throws Exception { + EventMessage emsg = + new EventMessage( + EventMessage.ID3_SCHEME_ID, + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + encodeTxxxId3Frame("Test description", "Test value")); + + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + + assertThat(metadata).hasSize(1); + assertThat(metadata.get(0).length()).isEqualTo(1); + TextInformationFrame expectedId3Frame = + new TextInformationFrame("TXXX", "Test description", "Test value"); + assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame); + } + + @Test + public void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception { + EventMessage emsg = + new EventMessage( + EventMessage.ID3_SCHEME_ID, + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + "Not a real ID3 tag".getBytes(ISO_8859_1)); + + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + + assertThat(metadata).isEmpty(); + } + + private static List runRenderer(byte[] input) throws ExoPlaybackException { + List metadata = new ArrayList<>(); + MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); + renderer.replaceStream( + new Format[] {EMSG_FORMAT}, + new FakeSampleStream(EMSG_FORMAT, /* eventDispatcher= */ null, input), + /* offsetUs= */ 0L); + renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format + renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data + + return Collections.unmodifiableList(metadata); + } + + /** + * Builds an ID3v2 tag containing a single 'user defined text information frame' (id='TXXX') with + * {@code description} and {@code value}. + * + * + */ + private static byte[] encodeTxxxId3Frame(String description, String value) { + byte[] id3FrameData = + TestUtil.joinByteArrays( + "TXXX".getBytes(ISO_8859_1), // ID for a 'user defined text information frame' + TestUtil.createByteArray(0, 0, 0, 0), // Frame size (set later) + TestUtil.createByteArray(0, 0), // Frame flags + TestUtil.createByteArray(0), // Character encoding = ISO-8859-1 + description.getBytes(ISO_8859_1), + TestUtil.createByteArray(0), // String null terminator + value.getBytes(ISO_8859_1), + TestUtil.createByteArray(0)); // String null terminator + int frameSizeIndex = 7; + int frameSize = id3FrameData.length - 10; + Assertions.checkArgument( + frameSize < 128, "frameSize must fit in 7 bits to avoid synch-safe encoding: " + frameSize); + id3FrameData[frameSizeIndex] = (byte) frameSize; + + byte[] id3Bytes = + TestUtil.joinByteArrays( + "ID3".getBytes(ISO_8859_1), // identifier + TestUtil.createByteArray(0x04, 0x00), // version + TestUtil.createByteArray(0), // Tag flags + TestUtil.createByteArray(0, 0, 0, 0), // Tag size (set later) + id3FrameData); + int tagSizeIndex = 9; + int tagSize = id3Bytes.length - 10; + Assertions.checkArgument( + tagSize < 128, "tagSize must fit in 7 bits to avoid synch-safe encoding: " + tagSize); + id3Bytes[tagSizeIndex] = (byte) tagSize; + return id3Bytes; + } +} From e267550d95a72ba7261eddad6dea8f8ce379e8e8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Aug 2019 14:17:18 +0100 Subject: [PATCH 332/807] Throw for unsupported libvpx output formats Currently we fail silently for high bit depth output when using ANativeWindow output mode. PiperOrigin-RevId: 263549384 --- extensions/vp9/src/main/jni/vpx_jni.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 9fc8b09a18..d23f07a90b 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -594,8 +594,14 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength); memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength); } - } else if (outputMode == kOutputModeSurfaceYuv && - img->fmt != VPX_IMG_FMT_I42016) { + } else if (outputMode == kOutputModeSurfaceYuv) { + if (img->fmt & VPX_IMG_FMT_HIGHBITDEPTH) { + LOGE( + "ERROR: High bit depth output format %d not supported in surface " + "YUV output mode", + img->fmt); + return -1; + } int id = *(int*)img->fb_priv; context->buffer_manager->add_ref(id); JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id); From 67477144796dce618b7de16925a8b1f1f664a820 Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Fri, 16 Aug 2019 10:37:48 +0530 Subject: [PATCH 333/807] Upgrade librtmp-client to 3.1.0 --- extensions/rtmp/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index 74ef70fbf0..9c709305bf 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'net.butterflytv.utils:rtmp-client:3.0.1' + implementation 'net.butterflytv.utils:rtmp-client:3.1.0' implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion From 3198b9efacd8d6c2e0dd2607f067ee16b1d57ba5 Mon Sep 17 00:00:00 2001 From: sr1990 Date: Mon, 19 Aug 2019 18:07:56 -0700 Subject: [PATCH 334/807] Support negative value of the @r attrbute of S in SegmentTimeline element --- .../dash/manifest/DashManifestParser.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 2d503a8763..0516ef6cc9 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 @@ -243,7 +243,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, null); + segmentBase = parseSegmentList(xpp, null,durationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); } else { @@ -336,7 +336,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); + segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties, @@ -524,7 +524,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); + segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate( @@ -718,7 +718,8 @@ public class DashManifestParser extends DefaultHandler indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, @Nullable SegmentList parent) + protected SegmentList parseSegmentList(XmlPullParser xpp, @Nullable SegmentList parent, + long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -736,7 +737,7 @@ public class DashManifestParser extends DefaultHandler if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { - timeline = parseSegmentTimeline(xpp,timescale,duration); + timeline = parseSegmentTimeline(xpp,timescale,periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) { if (segments == null) { segments = new ArrayList<>(); @@ -1002,10 +1003,10 @@ public class DashManifestParser extends DefaultHandler elapsedTime = parseLong(xpp, "t", elapsedTime); long duration = parseLong(xpp, "d", C.TIME_UNSET); - //if repeat is -1 : length of each segment = duration / timescale and - // number of segments = periodDuration / length of each segment + //if repeat is negative : length of each segment = duration / timescale and + // number of segments = periodDuration / length of each segment int repeat = parseInt(xpp,"r",0); - int count = repeat != -1? 1 + repeat : (int) (((periodDurationMs / 1000) * timescale) / duration); + int count = repeat >= 0? 1 + repeat : (int) (((periodDurationMs / 1000) * timescale) / duration); for (int i = 0; i < count; i++) { segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration)); From efb0549416921af5c0202f403964421ca35b686b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 15 Aug 2019 14:42:14 +0100 Subject: [PATCH 335/807] Remove 2 deprecated SimpleExoPlayer constructors PiperOrigin-RevId: 263552552 --- .../android/exoplayer2/ExoPlayerFactory.java | 1 + .../android/exoplayer2/SimpleExoPlayer.java | 72 ------------------- 2 files changed, 1 insertion(+), 72 deletions(-) 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 ae5071717d..efe351c70a 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 @@ -296,6 +296,7 @@ public final class ExoPlayerFactory { drmSessionManager, bandwidthMeter, analyticsCollector, + Clock.DEFAULT, looper); } 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 9606cc0d48..ba63dee80e 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 @@ -324,78 +324,6 @@ public class SimpleExoPlayer extends BasePlayer @Nullable private PriorityTaskManager priorityTaskManager; private boolean isPriorityTaskManagerRegistered; - /** - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param bandwidthMeter The {@link BandwidthMeter} 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 looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link - * DrmSessionManager} to the {@link MediaSource} factories. - */ - @Deprecated - protected SimpleExoPlayer( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - BandwidthMeter bandwidthMeter, - @Nullable DrmSessionManager drmSessionManager, - Looper looper) { - this( - context, - renderersFactory, - trackSelector, - loadControl, - drmSessionManager, - bandwidthMeter, - new AnalyticsCollector(Clock.DEFAULT), - looper); - } - - /** - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @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 bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @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. - * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link - * DrmSessionManager} to the {@link MediaSource} factories. - */ - @Deprecated - protected SimpleExoPlayer( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, - BandwidthMeter bandwidthMeter, - AnalyticsCollector analyticsCollector, - Looper looper) { - this( - context, - renderersFactory, - trackSelector, - loadControl, - drmSessionManager, - bandwidthMeter, - analyticsCollector, - Clock.DEFAULT, - looper); - } - /** * @param context A {@link Context}. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. From bc655839ddd5ad869fa7921fcde552e88b5419ad Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 16 Aug 2019 08:04:18 +0100 Subject: [PATCH 336/807] Remove superfluous logging PiperOrigin-RevId: 263718527 --- extensions/vp9/src/main/jni/vpx_jni.cc | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index d23f07a90b..303672334d 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -342,7 +342,7 @@ class JniBufferManager { *fb = out_buffer->vpx_fb; int retVal = 0; if (!out_buffer->vpx_fb.data || all_buffer_count >= MAX_FRAMES) { - LOGE("ERROR: JniBufferManager get_buffer OOM."); + LOGE("JniBufferManager get_buffer OOM."); retVal = -1; } else { memset(fb->data, 0, fb->size); @@ -354,7 +354,7 @@ class JniBufferManager { JniFrameBuffer* get_buffer(int id) const { if (id < 0 || id >= all_buffer_count) { - LOGE("ERROR: JniBufferManager get_buffer invalid id %d.", id); + LOGE("JniBufferManager get_buffer invalid id %d.", id); return NULL; } return all_buffers[id]; @@ -362,7 +362,7 @@ class JniBufferManager { void add_ref(int id) { if (id < 0 || id >= all_buffer_count) { - LOGE("ERROR: JniBufferManager add_ref invalid id %d.", id); + LOGE("JniBufferManager add_ref invalid id %d.", id); return; } pthread_mutex_lock(&mutex); @@ -372,13 +372,13 @@ class JniBufferManager { int release(int id) { if (id < 0 || id >= all_buffer_count) { - LOGE("ERROR: JniBufferManager release invalid id %d.", id); + LOGE("JniBufferManager release invalid id %d.", id); return -1; } pthread_mutex_lock(&mutex); JniFrameBuffer* buffer = all_buffers[id]; if (!buffer->ref_count) { - LOGE("ERROR: JniBufferManager release, buffer already released."); + LOGE("JniBufferManager release, buffer already released."); pthread_mutex_unlock(&mutex); return -1; } @@ -444,7 +444,7 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, vpx_codec_err_t err = vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0); if (err) { - LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err); + LOGE("Failed to initialize libvpx decoder, error = %d.", err); errorCode = err; return 0; } @@ -452,20 +452,19 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, err = vpx_codec_control(context->decoder, VP9D_SET_ROW_MT, enableRowMultiThreadMode); if (err) { - LOGE("ERROR: Failed to enable row multi thread mode, error = %d.", err); + LOGE("Failed to enable row multi thread mode, error = %d.", err); } #endif if (disableLoopFilter) { err = vpx_codec_control(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true); if (err) { - LOGE("ERROR: Failed to shut off libvpx loop filter, error = %d.", err); + LOGE("Failed to shut off libvpx loop filter, error = %d.", err); } #ifdef VPX_CTRL_VP9_SET_LOOP_FILTER_OPT } else { err = vpx_codec_control(context->decoder, VP9D_SET_LOOP_FILTER_OPT, true); if (err) { - LOGE("ERROR: Failed to enable loop filter optimization, error = %d.", - err); + LOGE("Failed to enable loop filter optimization, error = %d.", err); } #endif } @@ -473,8 +472,7 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer, context->buffer_manager); if (err) { - LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.", - err); + LOGE("Failed to set libvpx frame buffer functions, error = %d.", err); } // Populate JNI References. @@ -500,7 +498,7 @@ DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { vpx_codec_decode(context->decoder, buffer, len, NULL, 0); errorCode = 0; if (status != VPX_CODEC_OK) { - LOGE("ERROR: vpx_codec_decode() failed, status= %d", status); + LOGE("vpx_codec_decode() failed, status= %d", status); errorCode = status; return -1; } @@ -597,8 +595,8 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { } else if (outputMode == kOutputModeSurfaceYuv) { if (img->fmt & VPX_IMG_FMT_HIGHBITDEPTH) { LOGE( - "ERROR: High bit depth output format %d not supported in surface " - "YUV output mode", + "High bit depth output format %d not supported in surface YUV output " + "mode", img->fmt); return -1; } From 9dd04baef67ac9082064978913f3a67011f9fff6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Aug 2019 10:51:19 +0100 Subject: [PATCH 337/807] Rollback of https://github.com/google/ExoPlayer/commit/9f55045eeb07120d5c001db48ef7c7c622089cbd *** Original commit *** Rollback of https://github.com/google/ExoPlayer/commit/bbe681a904d58fe1f84f6c7c6e3390d932c86249 *** Original commit *** PiperOrigin-RevId: 263736897 --- constants.gradle | 1 + library/core/build.gradle | 3 +-- library/core/proguard-rules.txt | 3 ++- .../com/google/android/exoplayer2/util/NonNullApi.java | 8 +++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/constants.gradle b/constants.gradle index b1c2c636c7..9510b8442e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,6 +25,7 @@ project.ext { autoServiceVersion = '1.0-rc4' checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' + kotlinAnnotationsVersion = '1.3.31' androidXTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/library/core/build.gradle b/library/core/build.gradle index fda2f079de..d6aee8a35d 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -61,8 +61,7 @@ dependencies { 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" + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 1f7a8d0ee7..ab3cc5fccd 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -58,5 +58,6 @@ (com.google.android.exoplayer2.upstream.DataSource$Factory); } -# Don't warn about checkerframework +# Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** +-dontwarn kotlin.annotations.jvm.** 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 index bd7a70eba0..7678710f18 100644 --- 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 @@ -20,8 +20,8 @@ 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; +import kotlin.annotations.jvm.MigrationStatus; +import kotlin.annotations.jvm.UnderMigration; /** * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless @@ -29,8 +29,6 @@ import javax.annotation.meta.TypeQualifierDefault; */ @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) +@UnderMigration(status = MigrationStatus.STRICT) @Retention(RetentionPolicy.CLASS) public @interface NonNullApi {} From 89dd10503436629bce46cf48a2c26bcb5b0bdd6d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Aug 2019 15:07:53 +0100 Subject: [PATCH 338/807] Update window/period doc in Timeline. Most users are likely to be only interested in the playlist items. So put the Window definition first and explicitly mention that this is a playlist item. PiperOrigin-RevId: 263764515 --- .../google/android/exoplayer2/Timeline.java | 140 +++++++++--------- 1 file changed, 68 insertions(+), 72 deletions(-) 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 32fa3a6e4b..8d5731da20 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 @@ -26,96 +26,92 @@ import com.google.android.exoplayer2.util.Assertions; * complex compositions of media such as playlists and streams with inserted ads. Instances are * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides * a snapshot of the current state. - *

          - * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single - * logical piece of media, for example a media file. It may also define groups of ads inserted into - * the media, along with information about whether those ads have been loaded and played. A window - * spans one or more periods, defining the region within those periods that's currently available - * for playback along with additional information such as whether seeking is supported within the - * window. Each window defines a default position, which is the position from which playback will - * start when the player starts playing the window. The following examples illustrate timelines for - * various use cases. + * + *

          A timeline consists of {@link Window Windows} and {@link Period Periods}. + * + *

            + *
          • A {@link Window} usually corresponds to one playlist item. It may span one or more periods + * and it defines the region within those periods that's currently available for playback. The + * window also provides additional information such as whether seeking is supported within the + * window and the default position, which is the position from which playback will start when + * the player starts playing the window. + *
          • A {@link Period} defines a single logical piece of media, for example a media file. It may + * also define groups of ads inserted into the media, along with information about whether + * those ads have been loaded and played. + *
          + * + *

          The following examples illustrate timelines for various use cases. * *

          Single media file or on-demand stream

          - *

          - * Example timeline for a single file - *

          - * A timeline for a single media file or on-demand stream consists of a single period and window. - * The window spans the whole period, indicating that all parts of the media are available for - * playback. The window's default position is typically at the start of the period (indicated by the - * black dot in the figure above). + * + *

          Example timeline for a
+ * single file A timeline for a single media file or on-demand stream consists of a single period + * and window. The window spans the whole period, indicating that all parts of the media are + * available for playback. The window's default position is typically at the start of the period + * (indicated by the black dot in the figure above). * *

          Playlist of media files or on-demand streams

          - *

          - * Example timeline for a playlist of files - *

          - * A timeline for a playlist of media files or on-demand streams consists of multiple periods, each - * with its own window. Each window spans the whole of the corresponding period, and typically has a - * default position at the start of the period. The properties of the periods and windows (e.g. - * their durations and whether the window is seekable) will often only become known when the player - * starts buffering the corresponding file or stream. + * + *

          Example timeline for a playlist
+ * of files A timeline for a playlist of media files or on-demand streams consists of multiple + * periods, each with its own window. Each window spans the whole of the corresponding period, and + * typically has a default position at the start of the period. The properties of the periods and + * windows (e.g. their durations and whether the window is seekable) will often only become known + * when the player starts buffering the corresponding file or stream. * *

          Live stream with limited availability

          - *

          - * Example timeline for a live stream with
- *       limited availability - *

          - * A timeline for a live stream consists of a period whose duration is unknown, since it's - * continually extending as more content is broadcast. If content only remains available for a - * limited period of time then the window may start at a non-zero position, defining the region of - * content that can still be played. The window will have {@link Window#isDynamic} set to true if - * the stream is still live. Its default position is typically near to the live edge (indicated by - * the black dot in the figure above). + * + *

          Example timeline for a live
+ * stream with limited availability A timeline for a live stream consists of a period whose + * duration is unknown, since it's continually extending as more content is broadcast. If content + * only remains available for a limited period of time then the window may start at a non-zero + * position, defining the region of content that can still be played. The window will have {@link + * Window#isDynamic} set to true if the stream is still live. Its default position is typically near + * to the live edge (indicated by the black dot in the figure above). * *

          Live stream with indefinite availability

          - *

          - * Example timeline for a live stream with
- *       indefinite availability - *

          - * A timeline for a live stream with indefinite availability is similar to the - * Live stream with limited availability case, except that the window - * starts at the beginning of the period to indicate that all of the previously broadcast content - * can still be played. + * + *

          Example timeline for a
+ * live stream with indefinite availability A timeline for a live stream with indefinite + * availability is similar to the Live stream with limited availability + * case, except that the window starts at the beginning of the period to indicate that all of the + * previously broadcast content can still be played. * *

          Live stream with multiple periods

          - *

          - * Example timeline for a live stream
- *       with multiple periods - *

          - * This case arises when a live stream is explicitly divided into separate periods, for example at - * content boundaries. This case is similar to the Live stream with limited - * availability case, except that the window may span more than one period. Multiple periods are - * also possible in the indefinite availability case. + * + *

          Example timeline for a
+ * live stream with multiple periods This case arises when a live stream is explicitly divided + * into separate periods, for example at content boundaries. This case is similar to the Live stream with limited availability case, except that the window may + * span more than one period. Multiple periods are also possible in the indefinite availability + * case. * *

          On-demand stream followed by live stream

          - *

          - * Example timeline for an on-demand stream
- *       followed by a live stream - *

          - * This case is the concatenation of the Single media file or on-demand - * stream and Live stream with multiple periods cases. When playback - * of the on-demand stream ends, playback of the live stream will start from its default position - * near the live edge. + * + *

          Example timeline for an
+ * on-demand stream followed by a live stream This case is the concatenation of the Single media file or on-demand stream and Live + * stream with multiple periods cases. When playback of the on-demand stream ends, playback of + * the live stream will start from its default position near the live edge. * *

          On-demand stream with mid-roll ads

          - *

          - * Example timeline for an on-demand
- *       stream with mid-roll ad groups - *

          - * This case includes mid-roll ad groups, which are defined as part of the timeline's single period. - * The period can be queried for information about the ad groups and the ads they contain. + * + *

          Example timeline
+ * for an on-demand stream with mid-roll ad groups This case includes mid-roll ad groups, which + * are defined as part of the timeline's single period. The period can be queried for information + * about the ad groups and the ads they contain. */ public abstract class Timeline { /** - * Holds information about a window in a {@link Timeline}. A window defines a region of media - * currently available for playback along with additional information such as whether seeking is - * supported within the window. The figure below shows some of the information defined by a - * window, as well as how this information relates to corresponding {@link Period}s in the - * timeline. - *

          - * Information defined by a timeline window - *

          + * Holds information about a window in a {@link Timeline}. A window usually corresponds to one + * playlist item and defines a region of media currently available for playback along with + * additional information such as whether seeking is supported within the window. The figure below + * shows some of the information defined by a window, as well as how this information relates to + * corresponding {@link Period Periods} in the timeline. + * + *

          Information defined by a
+   * timeline window */ public static final class Window { From 01f484cbe965f7858f9621c97fec94d30eef188a Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 16 Aug 2019 15:31:05 +0100 Subject: [PATCH 339/807] Modify EventMessageDecoder to return null if decoding fails (currently throws exceptions) This matches the documentation on MetadataDecoder.decode: "@return The decoded metadata object, or null if the metadata could not be decoded." PiperOrigin-RevId: 263767144 --- .../metadata/emsg/EventMessageDecoder.java | 29 +++++++++++++------ .../metadata/MetadataRendererTest.java | 20 +++++++++---- .../source/dash/PlayerEmsgHandler.java | 3 ++ 3 files changed, 37 insertions(+), 15 deletions(-) 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 a1196c41c8..bbf7476d25 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -28,21 +29,31 @@ public final class EventMessageDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); byte[] data = buffer.array(); int size = buffer.limit(); - return new Metadata(decode(new ParsableByteArray(data, size))); + EventMessage decodedEventMessage = decode(new ParsableByteArray(data, size)); + if (decodedEventMessage == null) { + return null; + } else { + return new Metadata(decodedEventMessage); + } } + @Nullable public EventMessage decode(ParsableByteArray emsgData) { - String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long durationMs = emsgData.readUnsignedInt(); - long id = emsgData.readUnsignedInt(); - byte[] messageData = - Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); - return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + try { + String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + long durationMs = emsgData.readUnsignedInt(); + long id = emsgData.readUnsignedInt(); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + } catch (RuntimeException e) { + return null; + } } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index 4de8bb76cc..26dcefc611 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -55,13 +55,20 @@ public class MetadataRendererTest { /* id= */ 0, "Test data".getBytes(UTF_8)); - List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); assertThat(metadata.get(0).get(0)).isEqualTo(emsg); } + @Test + public void decodeMetadata_skipsMalformed() throws Exception { + List metadata = runRenderer(EMSG_FORMAT, "not valid emsg bytes".getBytes(UTF_8)); + + assertThat(metadata).isEmpty(); + } + @Test public void decodeMetadata_handlesWrappedMetadata() throws Exception { EventMessage emsg = @@ -72,7 +79,7 @@ public class MetadataRendererTest { /* id= */ 0, encodeTxxxId3Frame("Test description", "Test value")); - List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); @@ -91,17 +98,18 @@ public class MetadataRendererTest { /* id= */ 0, "Not a real ID3 tag".getBytes(ISO_8859_1)); - List metadata = runRenderer(eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); assertThat(metadata).isEmpty(); } - private static List runRenderer(byte[] input) throws ExoPlaybackException { + private static List runRenderer(Format format, byte[] input) + throws ExoPlaybackException { List metadata = new ArrayList<>(); MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); renderer.replaceStream( - new Format[] {EMSG_FORMAT}, - new FakeSampleStream(EMSG_FORMAT, /* eventDispatcher= */ null, input), + new Format[] {format}, + new FakeSampleStream(format, /* eventDispatcher= */ null, input), /* offsetUs= */ 0L); renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data 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 af4bf3ad70..883ca7420e 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 @@ -360,6 +360,9 @@ public final class PlayerEmsgHandler implements Handler.Callback { } long eventTimeUs = inputBuffer.timeUs; Metadata metadata = decoder.decode(inputBuffer); + if (metadata == null) { + continue; + } EventMessage eventMessage = (EventMessage) metadata.get(0); if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { parsePlayerEmsgEvent(eventTimeUs, eventMessage); From 424f9910790ad2d83a66de71cf8b42fdeae1497a Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 16 Aug 2019 15:39:57 +0100 Subject: [PATCH 340/807] Extend EventMessage.toString to include durationMs This field is used in .equals(), so it makes sense to include it in toString() too. PiperOrigin-RevId: 263768329 --- .../android/exoplayer2/metadata/emsg/EventMessage.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index c9e9d54093..7d35a15e31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -135,7 +135,14 @@ public final class EventMessage implements Metadata.Entry { @Override public String toString() { - return "EMSG: scheme=" + schemeIdUri + ", id=" + id + ", value=" + value; + return "EMSG: scheme=" + + schemeIdUri + + ", id=" + + id + + ", durationMs=" + + durationMs + + ", value=" + + value; } // Parcelable implementation. From 14f77cb8b133fe3e6807542827fc3990e798c79b Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 16 Aug 2019 15:40:43 +0100 Subject: [PATCH 341/807] Unwrap SCTE-35 messages in emsg boxes PiperOrigin-RevId: 263768428 --- .../metadata/emsg/EventMessage.java | 25 ++++++++--- .../metadata/MetadataRendererTest.java | 43 ++++++++++++++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 7d35a15e31..6e0b0b40f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -35,13 +35,21 @@ public final class EventMessage implements Metadata.Entry { @VisibleForTesting public static final String ID3_SCHEME_ID = "https://developer.apple.com/streaming/emsg-id3"; + /** + * scheme_id_uri from section 7.3.2 of SCTE 214-3 + * 2015. + */ + @VisibleForTesting public static final String SCTE35_SCHEME_ID = "urn:scte:scte35:2014:bin"; + private static final Format ID3_FORMAT = Format.createSampleFormat( /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + private static final Format SCTE35_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_SCTE35, Format.OFFSET_SAMPLE_RELATIVE); - /** - * The message scheme. - */ + /** The message scheme. */ public final String schemeIdUri; /** @@ -94,13 +102,20 @@ public final class EventMessage implements Metadata.Entry { @Override @Nullable public Format getWrappedMetadataFormat() { - return ID3_SCHEME_ID.equals(schemeIdUri) ? ID3_FORMAT : null; + switch (schemeIdUri) { + case ID3_SCHEME_ID: + return ID3_FORMAT; + case SCTE35_SCHEME_ID: + return SCTE35_FORMAT; + default: + return null; + } } @Override @Nullable public byte[] getWrappedMetadataBytes() { - return ID3_SCHEME_ID.equals(schemeIdUri) ? messageData : null; + return getWrappedMetadataFormat() != null ? messageData : null; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index 26dcefc611..af6489f726 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.metadata.scte35.TimeSignalCommand; import com.google.android.exoplayer2.testutil.FakeSampleStream; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Assertions; @@ -40,6 +41,28 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MetadataRendererTest { + private static final byte[] SCTE35_TIME_SIGNAL_BYTES = + TestUtil.joinByteArrays( + TestUtil.createByteArray( + 0, // table_id. + 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x14, // section_length(8). + 0x00, // protocol_version. + 0x00), // encrypted_packet, encryption_algorithm, pts_adjustment(1). + TestUtil.createByteArray(0x00, 0x00, 0x00, 0x00), // pts_adjustment(32). + TestUtil.createByteArray( + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x05, // splice_command_length(8). + 0x06, // splice_command_type = time_signal. + // Start of splice_time(). + 0x80), // time_specified_flag, reserved, pts_time(1). + TestUtil.createByteArray( + 0x52, 0x03, 0x02, 0x8f), // pts_time(32). PTS for a second after playback position. + TestUtil.createByteArray( + 0x00, 0x00, 0x00, 0x00)); // CRC_32 (ignored, check happens at extraction). + private static final Format EMSG_FORMAT = Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); @@ -70,7 +93,7 @@ public class MetadataRendererTest { } @Test - public void decodeMetadata_handlesWrappedMetadata() throws Exception { + public void decodeMetadata_handlesId3WrappedInEmsg() throws Exception { EventMessage emsg = new EventMessage( EventMessage.ID3_SCHEME_ID, @@ -88,6 +111,24 @@ public class MetadataRendererTest { assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame); } + @Test + public void decodeMetadata_handlesScte35WrappedInEmsg() throws Exception { + + EventMessage emsg = + new EventMessage( + EventMessage.SCTE35_SCHEME_ID, + /* value= */ "", + /* durationMs= */ 1, + /* id= */ 0, + SCTE35_TIME_SIGNAL_BYTES); + + List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); + + assertThat(metadata).hasSize(1); + assertThat(metadata.get(0).length()).isEqualTo(1); + assertThat(metadata.get(0).get(0)).isInstanceOf(TimeSignalCommand.class); + } + @Test public void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception { EventMessage emsg = From 652c2f9c188bf9d9d6e323ff5333e5026454a082 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Aug 2019 16:32:49 +0100 Subject: [PATCH 342/807] Advance playing period even if the next one isn't prepared yet. This solves various issues around event association for buffering and error throwing around period discontinuities. The main changes are: - Logic around being "ready" at the end of a period no longer checks if the next period is prepared. - Advancing the playing period no longer checks if the next one is prepared. - Prepare errors are always thrown for the playing period. This changes the semantics and assumptions about the "playing" period: 1. The playing period can no longer assumed to be prepared. 2. We no longer have a case where the queue is non-empty and the playing or reading periods are unassigned (=null). Most other code changes ensure that these changed assumptions are handled. Issue:#5407 PiperOrigin-RevId: 263776304 --- RELEASENOTES.md | 2 + .../exoplayer2/ExoPlayerImplInternal.java | 187 +++++++++--------- .../android/exoplayer2/MediaPeriodQueue.java | 76 +++---- .../exoplayer2/MediaPeriodQueueTest.java | 8 +- 4 files changed, 131 insertions(+), 142 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d166fb41c6..3489c34bbc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,6 +38,8 @@ `ExoPlayer.Builder`. * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` ([#5619](https://github.com/google/ExoPlayer/issues/5619)). +* Fix issue where player errors are thrown too early at playlist transitions + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). ### 2.10.4 ### 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 6ab0838e26..53c381961e 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 @@ -83,8 +83,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; - private static final int PREPARING_SOURCE_INTERVAL_MS = 10; - private static final int RENDERING_INTERVAL_MS = 10; + private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; private final Renderer[] renderers; @@ -514,22 +513,25 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updatePlaybackPositions() throws ExoPlaybackException { - if (!queue.hasPlayingPeriod()) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { return; } // Update the playback position. - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); - if (periodPositionUs != C.TIME_UNSET) { - resetRendererPosition(periodPositionUs); + long discontinuityPositionUs = + playingPeriodHolder.prepared + ? playingPeriodHolder.mediaPeriod.readDiscontinuity() + : C.TIME_UNSET; + if (discontinuityPositionUs != C.TIME_UNSET) { + resetRendererPosition(discontinuityPositionUs); // A MediaPeriod may report a discontinuity at the current playback position to ensure the // renderers are flushed. Only report the discontinuity externally if the position changed. - if (periodPositionUs != playbackInfo.positionUs) { + if (discontinuityPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.copyWithNewPosition( playbackInfo.periodId, - periodPositionUs, + discontinuityPositionUs, playbackInfo.contentPositionUs, getTotalBufferedDurationUs()); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); @@ -538,7 +540,7 @@ import java.util.concurrent.atomic.AtomicBoolean; rendererPositionUs = mediaClock.syncAndGetPositionUs( /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); - periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); playbackInfo.positionUs = periodPositionUs; } @@ -552,60 +554,71 @@ import java.util.concurrent.atomic.AtomicBoolean; private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = clock.uptimeMillis(); updatePeriods(); - if (!queue.hasPlayingPeriod()) { - // We're still waiting for the first period to be prepared. - maybeThrowPeriodPrepareError(); - scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); + + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { + // We're still waiting until the playing period is available. + scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); return; } - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); - long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; - - playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, - retainBackBufferFromKeyframe); boolean renderersEnded = true; - boolean renderersReadyOrEnded = true; - for (Renderer renderer : enabledRenderers) { - // TODO: Each renderer should return the maximum delay before which it wishes to be called - // again. The minimum of these values should then be used as the delay before the next - // invocation of this method. - renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); - renderersEnded = renderersEnded && renderer.isEnded(); - // Determine whether the renderer is ready (or ended). We override to assume the renderer is - // ready if it needs the next sample stream. This is necessary to avoid getting stuck if - // tracks in the current period have uneven durations. See: - // https://github.com/google/ExoPlayer/issues/1874 - boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded() - || rendererWaitingForNextStream(renderer); - if (!rendererReadyOrEnded) { - renderer.maybeThrowStreamError(); + boolean renderersAllowPlayback = true; + if (playingPeriodHolder.prepared) { + long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + playingPeriodHolder.mediaPeriod.discardBuffer( + playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + if (renderer.getState() == Renderer.STATE_DISABLED) { + continue; + } + // TODO: Each renderer should return the maximum delay before which it wishes to be called + // again. The minimum of these values should then be used as the delay before the next + // invocation of this method. + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); + renderersEnded = renderersEnded && renderer.isEnded(); + // Determine whether the renderer allows playback to continue. Playback can continue if the + // renderer is ready or ended. Also continue playback if the renderer is reading ahead into + // the next stream or is waiting for the next stream. This is to avoid getting stuck if + // tracks in the current period have uneven durations and are still being read by another + // renderer. See: https://github.com/google/ExoPlayer/issues/1874. + boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); + boolean isWaitingForNextStream = + !isReadingAhead + && playingPeriodHolder.getNext() != null + && renderer.hasReadStreamToEnd(); + boolean allowsPlayback = + isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); + renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; + if (!allowsPlayback) { + renderer.maybeThrowStreamError(); + } } - renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded; - } - if (!renderersReadyOrEnded) { - maybeThrowPeriodPrepareError(); + } else { + playingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); } long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; if (renderersEnded + && playingPeriodHolder.prepared && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); } else if (playbackInfo.playbackState == Player.STATE_BUFFERING - && shouldTransitionToReadyState(renderersReadyOrEnded)) { + && shouldTransitionToReadyState(renderersAllowPlayback)) { setState(Player.STATE_READY); if (playWhenReady) { startRenderers(); } } else if (playbackInfo.playbackState == Player.STATE_READY - && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) { + && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) { rebuffering = playWhenReady; setState(Player.STATE_BUFFERING); stopRenderers(); @@ -619,7 +632,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) || playbackInfo.playbackState == Player.STATE_BUFFERING) { - scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); + scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { @@ -681,7 +694,9 @@ import java.util.concurrent.atomic.AtomicBoolean; long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - if (playingPeriodHolder != null && newPeriodPositionUs != 0) { + if (playingPeriodHolder != null + && playingPeriodHolder.prepared + && newPeriodPositionUs != 0) { newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( newPeriodPositionUs, seekParameters); @@ -771,10 +786,11 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { + MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod(); rendererPositionUs = - !queue.hasPlayingPeriod() + playingMediaPeriod == null ? periodPositionUs - : queue.getPlayingPeriod().toRendererTime(periodPositionUs); + : playingMediaPeriod.toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); @@ -1092,10 +1108,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void reselectTracksInternal() throws ExoPlaybackException { - if (!queue.hasPlayingPeriod()) { - // We don't have tracks yet, so we don't care. - return; - } float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); @@ -1182,8 +1194,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); - while (periodHolder != null && periodHolder.prepared) { + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + while (periodHolder != null) { TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); for (TrackSelection trackSelection : trackSelections) { if (trackSelection != null) { @@ -1195,7 +1207,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void notifyTrackSelectionDiscontinuity() { - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); while (periodHolder != null) { TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); for (TrackSelection trackSelection : trackSelections) { @@ -1230,12 +1242,10 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean isTimelineReady() { MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder nextPeriodHolder = playingPeriodHolder.getNext(); long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; - return playingPeriodDurationUs == C.TIME_UNSET - || playbackInfo.positionUs < playingPeriodDurationUs - || (nextPeriodHolder != null - && (nextPeriodHolder.prepared || nextPeriodHolder.info.id.isAd())); + return playingPeriodHolder.prepared + && (playingPeriodDurationUs == C.TIME_UNSET + || playbackInfo.positionUs < playingPeriodDurationUs); } private void maybeThrowSourceInfoRefreshError() throws IOException { @@ -1251,21 +1261,6 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaSource.maybeThrowSourceInfoRefreshError(); } - private void maybeThrowPeriodPrepareError() throws IOException { - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - if (loadingPeriodHolder != null - && !loadingPeriodHolder.prepared - && (readingPeriodHolder == null || readingPeriodHolder.getNext() == loadingPeriodHolder)) { - for (Renderer renderer : enabledRenderers) { - if (!renderer.hasReadStreamToEnd()) { - return; - } - } - loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); - } - } - private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) throws ExoPlaybackException { if (sourceRefreshInfo.source != mediaSource) { @@ -1335,7 +1330,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } else { // Something changed. Seek to new start position. - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); if (periodHolder != null) { // Update the new playing media period info if it already exists. while (periodHolder.getNext() != null) { @@ -1361,6 +1356,9 @@ import java.util.concurrent.atomic.AtomicBoolean; return 0; } long maxReadPositionUs = readingHolder.getRendererOffset(); + if (!readingHolder.prepared) { + return maxReadPositionUs; + } for (int i = 0; i < renderers.length; i++) { if (renderers[i].getState() == Renderer.STATE_DISABLED || renderers[i].getStream() != readingHolder.sampleStreams[i]) { @@ -1494,23 +1492,26 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeUpdatePlayingPeriod(); } - private void maybeUpdateLoadingPeriod() throws IOException { + private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException { queue.reevaluateBuffer(rendererPositionUs); if (queue.shouldLoadNextMediaPeriod()) { MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); if (info == null) { maybeThrowSourceInfoRefreshError(); } else { - MediaPeriod mediaPeriod = - queue.enqueueNextMediaPeriod( + MediaPeriodHolder mediaPeriodHolder = + queue.enqueueNextMediaPeriodHolder( rendererCapabilities, trackSelector, loadControl.getAllocator(), mediaSource, info, emptyTrackSelectorResult); - mediaPeriod.prepare(this, info.startPositionUs); + mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); + if (queue.getPlayingPeriod() == mediaPeriodHolder) { + resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime()); + } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } } @@ -1522,7 +1523,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { + private void maybeUpdateReadingPeriod() throws ExoPlaybackException { MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; @@ -1552,7 +1553,6 @@ import java.util.concurrent.atomic.AtomicBoolean; if (!readingPeriodHolder.getNext().prepared) { // The successor is not prepared yet. - maybeThrowPeriodPrepareError(); return; } @@ -1607,6 +1607,11 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeNotifyPlaybackInfoChanged(); } MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + if (oldPlayingPeriodHolder == queue.getReadingPeriod()) { + // The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams + // anymore and need to re-enable the renderers. Set all current streams final to do that. + setAllRendererStreamsFinal(); + } MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); updatePlayingPeriodRenderers(oldPlayingPeriodHolder); playbackInfo = @@ -1633,17 +1638,22 @@ import java.util.concurrent.atomic.AtomicBoolean; if (playingPeriodHolder == null) { return false; } - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - if (playingPeriodHolder == readingPeriodHolder) { + MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); + if (nextPlayingPeriodHolder == null) { + return false; + } + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) { return false; } - MediaPeriodHolder nextPlayingPeriodHolder = - Assertions.checkNotNull(playingPeriodHolder.getNext()); return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime(); } private boolean hasReadingPeriodFinishedReading() { MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (!readingPeriodHolder.prepared) { + return false; + } for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; @@ -1674,10 +1684,9 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaClock.getPlaybackParameters().speed, playbackInfo.timeline); updateLoadControlTrackSelection( loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult()); - if (!queue.hasPlayingPeriod()) { - // This is the first prepared period, so start playing it. - MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); - resetRendererPosition(playingPeriodHolder.info.startPositionUs); + if (loadingPeriodHolder == queue.getPlayingPeriod()) { + // This is the first prepared period, so update the position and the renderers. + resetRendererPosition(loadingPeriodHolder.info.startPositionUs); updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null); } maybeContinueLoading(); @@ -1805,12 +1814,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private boolean rendererWaitingForNextStream(Renderer renderer) { - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - MediaPeriodHolder nextPeriodHolder = readingPeriodHolder.getNext(); - return nextPeriodHolder != null && nextPeriodHolder.prepared && renderer.hasReadStreamToEnd(); - } - private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) { MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod(); MediaPeriodId loadingMediaPeriodId = 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 0f279ba6d3..e515877d78 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 @@ -128,8 +128,8 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Enqueues a new media period based on the specified information as the new loading media period, - * and returns it. + * Enqueues a new media period holder based on the specified information as the new loading media + * period, and returns it. * * @param rendererCapabilities The renderer capabilities. * @param trackSelector The track selector. @@ -139,7 +139,7 @@ import com.google.android.exoplayer2.util.Assertions; * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. */ - public MediaPeriod enqueueNextMediaPeriod( + public MediaPeriodHolder enqueueNextMediaPeriodHolder( RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, @@ -162,13 +162,15 @@ import com.google.android.exoplayer2.util.Assertions; info, emptyTrackSelectorResult); if (loading != null) { - Assertions.checkState(hasPlayingPeriod()); loading.setNext(newPeriodHolder); + } else { + playing = newPeriodHolder; + reading = newPeriodHolder; } oldFrontPeriodUid = null; loading = newPeriodHolder; length++; - return newPeriodHolder.mediaPeriod; + return newPeriodHolder; } /** @@ -182,36 +184,19 @@ 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. + * empty. */ @Nullable public MediaPeriodHolder getPlayingPeriod() { return playing; } - /** - * Returns the reading period holder, or null if the queue is empty or the player hasn't started - * reading. - */ + /** Returns the reading period holder, or null if the queue is empty. */ @Nullable public MediaPeriodHolder getReadingPeriod() { return reading; } - /** - * 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; - } - - /** Returns whether the reading and playing period holders are set. */ - public boolean hasPlayingPeriod() { - return playing != null; - } - /** * Continues reading from the next period holder in the queue. * @@ -225,29 +210,26 @@ import com.google.android.exoplayer2.util.Assertions; /** * Dequeues the playing period holder from the front of the queue and advances the playing period - * holder to be the next item in the queue. If the playing period holder is unset, set it to the - * item in the front of the queue. + * holder to be the next item in the queue. * * @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) { - reading = playing.getNext(); - } - playing.release(); - length--; - if (length == 0) { - loading = null; - oldFrontPeriodUid = playing.uid; - oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; - } - playing = playing.getNext(); - } else { - playing = loading; - reading = loading; + if (playing == null) { + return null; } + if (playing == reading) { + reading = playing.getNext(); + } + playing.release(); + length--; + if (length == 0) { + loading = null; + oldFrontPeriodUid = playing.uid; + oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; + } + playing = playing.getNext(); return playing; } @@ -283,7 +265,7 @@ import com.google.android.exoplayer2.util.Assertions; * of queue (typically the playing one) for later reuse. */ public void clear(boolean keepFrontPeriodUid) { - MediaPeriodHolder front = getFrontPeriod(); + MediaPeriodHolder front = playing; if (front != null) { oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; @@ -315,7 +297,7 @@ import com.google.android.exoplayer2.util.Assertions; // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. MediaPeriodHolder previousPeriodHolder = null; - MediaPeriodHolder periodHolder = getFrontPeriod(); + MediaPeriodHolder periodHolder = playing; while (periodHolder != null) { MediaPeriodInfo oldPeriodInfo = periodHolder.info; @@ -451,7 +433,7 @@ import com.google.android.exoplayer2.util.Assertions; } } } - MediaPeriodHolder mediaPeriodHolder = getFrontPeriod(); + MediaPeriodHolder mediaPeriodHolder = playing; while (mediaPeriodHolder != null) { if (mediaPeriodHolder.uid.equals(periodUid)) { // Reuse window sequence number of first exact period match. @@ -459,7 +441,7 @@ import com.google.android.exoplayer2.util.Assertions; } mediaPeriodHolder = mediaPeriodHolder.getNext(); } - mediaPeriodHolder = getFrontPeriod(); + mediaPeriodHolder = playing; while (mediaPeriodHolder != null) { int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid); if (indexOfHolderInTimeline != C.INDEX_UNSET) { @@ -496,7 +478,7 @@ import com.google.android.exoplayer2.util.Assertions; */ private boolean updateForPlaybackModeChange() { // Find the last existing period holder that matches the new period order. - MediaPeriodHolder lastValidPeriodHolder = getFrontPeriod(); + MediaPeriodHolder lastValidPeriodHolder = playing; if (lastValidPeriodHolder == null) { return true; } @@ -529,7 +511,7 @@ import com.google.android.exoplayer2.util.Assertions; lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); // If renderers may have read from a period that's been removed, it is necessary to restart. - return !readingPeriodRemoved || !hasPlayingPeriod(); + return !readingPeriodRemoved; } /** 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 14aa436be3..3c6c4462ca 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 @@ -370,7 +370,9 @@ public final class MediaPeriodQueueTest { private void advance() { enqueueNext(); - advancePlaying(); + if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { + advancePlaying(); + } } private void advancePlaying() { @@ -382,7 +384,7 @@ public final class MediaPeriodQueueTest { } private void enqueueNext() { - mediaPeriodQueue.enqueueNextMediaPeriod( + mediaPeriodQueue.enqueueNextMediaPeriodHolder( rendererCapabilities, trackSelector, allocator, @@ -460,7 +462,7 @@ public final class MediaPeriodQueueTest { private int getQueueLength() { int length = 0; - MediaPeriodHolder periodHolder = mediaPeriodQueue.getFrontPeriod(); + MediaPeriodHolder periodHolder = mediaPeriodQueue.getPlayingPeriod(); while (periodHolder != null) { length++; periodHolder = periodHolder.getNext(); From 20fd4e16d247739e6c046e8aac2f0d9d6a837057 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 19 Aug 2019 10:50:22 +0100 Subject: [PATCH 343/807] Deprecate setTag parameter in Timeline.getWindow. There is no point in having this parameter as the tag should always be a single immutable object instantiated at the time the Timeline is created or earlier. So there is no preformance benefit and it's error-prone in case people forget to set setTag=true. PiperOrigin-RevId: 264117041 --- RELEASENOTES.md | 1 + .../exoplayer2/ext/cast/CastTimeline.java | 6 +-- .../google/android/exoplayer2/BasePlayer.java | 8 +--- .../google/android/exoplayer2/Timeline.java | 38 +++++++++---------- .../source/AbstractConcatenatedTimeline.java | 6 +-- .../source/ClippingMediaSource.java | 6 +-- .../exoplayer2/source/ForwardingTimeline.java | 5 +-- .../exoplayer2/source/MaskingMediaSource.java | 3 +- .../source/SinglePeriodTimeline.java | 4 +- .../source/ads/SinglePeriodAdTimeline.java | 5 +-- .../source/SinglePeriodTimelineTest.java | 9 ++--- .../source/dash/DashMediaSource.java | 6 +-- .../exoplayer2/testutil/FakeTimeline.java | 6 +-- .../exoplayer2/testutil/TimelineAsserts.java | 6 +-- 14 files changed, 43 insertions(+), 66 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3489c34bbc..8415f57c9a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,7 @@ ([#5619](https://github.com/google/ExoPlayer/issues/5619)). * Fix issue where player errors are thrown too early at playlist transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). +* Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be set. ### 2.10.4 ### 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 b84f1c1f2b..58dbec611a 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 @@ -110,13 +110,11 @@ import java.util.Arrays; } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { long durationUs = durationsUs[windowIndex]; boolean isDynamic = durationUs == C.TIME_UNSET; - Object tag = setTag ? ids[windowIndex] : null; return window.set( - tag, + /* tag= */ ids[windowIndex], /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, 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 bb14ac147b..1b3e57cede 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 @@ -95,18 +95,14 @@ public abstract class BasePlayer implements Player { @Nullable public final Object getCurrentTag() { Timeline timeline = getCurrentTimeline(); - return timeline.isEmpty() - ? null - : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ true).tag; + return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).tag; } @Override @Nullable public final Object getCurrentManifest() { Timeline timeline = getCurrentTimeline(); - return timeline.isEmpty() - ? null - : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ false).manifest; + return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).manifest; } @Override 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 8d5731da20..57d3d8bf1d 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 @@ -520,8 +520,7 @@ public abstract class Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { throw new IndexOutOfBoundsException(); } @@ -633,28 +632,20 @@ public abstract class Timeline { } /** - * Populates a {@link Window} with data for the window at the specified index. Does not populate - * {@link Window#tag}. + * Populates a {@link Window} with data for the window at the specified index. * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. * @return The populated {@link Window}, for convenience. */ public final Window getWindow(int windowIndex, Window window) { - return getWindow(windowIndex, window, false); + return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); } - /** - * Populates a {@link Window} with data for the window at the specified index. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set - * to null. The caller should pass false for efficiency reasons unless the field is required. - * @return The populated {@link Window}, for convenience. - */ + /** @deprecated Use {@link #getWindow(int, Window)} instead. Tags will always be set. */ + @Deprecated public final Window getWindow(int windowIndex, Window window, boolean setTag) { - return getWindow(windowIndex, window, setTag, 0); + return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); } /** @@ -662,14 +653,21 @@ public abstract class Timeline { * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. - * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set - * to null. The caller should pass false for efficiency reasons unless the field is required. * @param defaultPositionProjectionUs A duration into the future that the populated window's * default start position should be projected. * @return The populated {@link Window}, for convenience. */ - public abstract Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs); + @SuppressWarnings("deprecation") + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return getWindow(windowIndex, window, /* setTag= */ true, defaultPositionProjectionUs); + } + + /** @deprecated Implement {@link #getWindow(int, Window, long)} instead and always set the tag. */ + @Deprecated + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + return getWindow(windowIndex, window, defaultPositionProjectionUs); + } /** * Returns the number of periods in the timeline. @@ -750,7 +748,7 @@ public abstract class Timeline { long windowPositionUs, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, getWindowCount()); - getWindow(windowIndex, window, false, defaultPositionProjectionUs); + getWindow(windowIndex, window, defaultPositionProjectionUs); if (windowPositionUs == C.TIME_UNSET) { windowPositionUs = window.getDefaultPositionUs(); if (windowPositionUs == C.TIME_UNSET) { 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 db19764318..4e7b572384 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 @@ -189,14 +189,12 @@ import com.google.android.exoplayer2.util.Assertions; } @Override - public final Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); getTimelineByChildIndex(childIndex) - .getWindow( - windowIndex - firstWindowIndexInChild, window, setTag, defaultPositionProjectionUs); + .getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInChild; window.lastPeriodIndex += firstPeriodIndexInChild; return window; 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 81169354de..703f8bafc0 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 @@ -342,10 +342,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - timeline.getWindow( - /* windowIndex= */ 0, window, setTag, /* defaultPositionProjectionUs= */ 0); + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + timeline.getWindow(/* windowIndex= */ 0, window, /* defaultPositionProjectionUs= */ 0); window.positionInFirstPeriodUs += startUs; window.durationUs = durationUs; window.isDynamic = isDynamic; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index 45997aced4..38b373b26c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -57,9 +57,8 @@ public abstract class ForwardingTimeline extends Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - return timeline.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs); + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); } @Override 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 index d9dd83de4f..35800f56da 100644 --- 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 @@ -287,8 +287,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { return window.set( tag, /* manifest= */ null, 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 8790b09f07..966f8e4c7a 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 @@ -159,10 +159,8 @@ public final class SinglePeriodTimeline extends Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); - Object tag = setTag ? this.tag : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; if (isDynamic && defaultPositionProjectionUs != 0) { if (windowDurationUs == C.TIME_UNSET) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index 25a1440c80..b5167dc173 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -55,9 +55,8 @@ public final class SinglePeriodAdTimeline extends ForwardingTimeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - window = super.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs); + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + window = super.getWindow(windowIndex, window, defaultPositionProjectionUs); if (window.durationUs == C.TIME_UNSET) { window.durationUs = adPlaybackState.contentDurationUs; } 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 701ec3521c..cb21db8212 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 @@ -88,8 +88,7 @@ public final class SinglePeriodTimelineTest { /* manifest= */ null, /* tag= */ null); - assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); - assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag).isNull(); + assertThat(timeline.getWindow(/* windowIndex= */ 0, window).tag).isNull(); assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ false).id).isNull(); assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).id).isNull(); assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ false).uid).isNull(); @@ -98,7 +97,7 @@ public final class SinglePeriodTimelineTest { } @Test - public void setTag_isUsedForWindowTag() { + public void getWindow_setsTag() { Object tag = new Object(); SinglePeriodTimeline timeline = new SinglePeriodTimeline( @@ -108,9 +107,7 @@ public final class SinglePeriodTimelineTest { /* manifest= */ null, tag); - assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); - assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag) - .isEqualTo(tag); + assertThat(timeline.getWindow(/* windowIndex= */ 0, window).tag).isEqualTo(tag); } @Test 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 890a272c5e..1b163c02e2 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 @@ -1207,18 +1207,16 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); - Object tag = setTag ? windowTag : null; boolean isDynamic = manifest.dynamic && manifest.minUpdatePeriodMs != C.TIME_UNSET && manifest.durationMs == C.TIME_UNSET; return window.set( - tag, + windowTag, manifest, presentationStartTimeMs, windowStartTimeMs, 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 58ee32cdd9..f0a0c77ff6 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 @@ -179,12 +179,10 @@ public final class FakeTimeline extends Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; - Object tag = setTag ? windowDefinition.id : null; return window.set( - tag, + /* tag= */ windowDefinition.id, manifests[windowIndex], /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 1e3c9c61d9..f3ec47a88b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -52,7 +52,7 @@ public final class TimelineAsserts { Window window = new Window(); assertThat(timeline.getWindowCount()).isEqualTo(expectedWindowTags.length); for (int i = 0; i < timeline.getWindowCount(); i++) { - timeline.getWindow(i, window, true); + timeline.getWindow(i, window); if (expectedWindowTags[i] != null) { assertThat(window.tag).isEqualTo(expectedWindowTags[i]); } @@ -63,7 +63,7 @@ public final class TimelineAsserts { public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { Window window = new Window(); for (int i = 0; i < timeline.getWindowCount(); i++) { - timeline.getWindow(i, window, true); + timeline.getWindow(i, window); assertThat(window.isDynamic).isEqualTo(windowIsDynamic[i]); } } @@ -129,7 +129,7 @@ public final class TimelineAsserts { Window window = new Window(); Period period = new Period(); for (int i = 0; i < windowCount; i++) { - timeline.getWindow(i, window, true); + timeline.getWindow(i, window); assertThat(window.firstPeriodIndex).isEqualTo(accumulatedPeriodCounts[i]); assertThat(window.lastPeriodIndex).isEqualTo(accumulatedPeriodCounts[i + 1] - 1); } From 17d8e3728fa4fa42898ac0c81c971a9d69bb143e Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 19 Aug 2019 12:03:55 +0100 Subject: [PATCH 344/807] Add support for the AOM scheme_id for ID3-in-EMSG https://developer.apple.com/documentation/http_live_streaming/about_the_common_media_application_format_with_http_live_streaming PiperOrigin-RevId: 264126140 --- .../metadata/emsg/EventMessage.java | 20 +++++++++++++------ .../metadata/MetadataRendererTest.java | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 6e0b0b40f2..7e3862ca31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -27,13 +27,20 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; -/** - * An Event Message (emsg) as defined in ISO 23009-1. - */ +/** An Event Message (emsg) as defined in ISO 23009-1. */ public final class EventMessage implements Metadata.Entry { - @VisibleForTesting - public static final String ID3_SCHEME_ID = "https://developer.apple.com/streaming/emsg-id3"; + /** + * emsg scheme_id_uri from the CMAF + * spec. + */ + @VisibleForTesting public static final String ID3_SCHEME_ID_AOM = "https://aomedia.org/emsg/ID3"; + + /** + * The Apple-hosted scheme_id equivalent to {@code ID3_SCHEME_ID_AOM} - used before AOM adoption. + */ + private static final String ID3_SCHEME_ID_APPLE = + "https://developer.apple.com/streaming/emsg-id3"; /** * scheme_id_uri from section 7.3.2 of Date: Mon, 19 Aug 2019 14:48:42 +0100 Subject: [PATCH 345/807] Extend ExoPlayer builders with useLazyPreparations and AnalyticsCollector. Both values are needed to set-up the ExoPlayer instances for playlists. PiperOrigin-RevId: 264146052 --- .../google/android/exoplayer2/ExoPlayer.java | 43 +++++++++++++++++++ .../android/exoplayer2/SimpleExoPlayer.java | 23 ++++++++++ 2 files changed, 66 insertions(+) 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 991be9b08b..2bed5d6f8b 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 @@ -19,6 +19,7 @@ import android.content.Context; import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; @@ -138,6 +139,8 @@ public interface ExoPlayer extends Player { private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private Looper looper; + private AnalyticsCollector analyticsCollector; + private boolean useLazyPreparation; private boolean buildCalled; /** @@ -152,6 +155,8 @@ public interface ExoPlayer extends Player { *

        • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link * Looper} of the application's main thread if the current thread doesn't have a {@link * Looper} + *
        • {@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT} + *
        • {@code useLazyPreparation}: {@code true} *
        • {@link Clock}: {@link Clock#DEFAULT} *
        * @@ -165,6 +170,8 @@ public interface ExoPlayer extends Player { new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -180,6 +187,8 @@ public interface ExoPlayer extends Player { * @param loadControl A {@link LoadControl}. * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. */ public Builder( @@ -188,6 +197,8 @@ public interface ExoPlayer extends Player { LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper, + AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock) { Assertions.checkArgument(renderers.length > 0); this.renderers = renderers; @@ -195,6 +206,8 @@ public interface ExoPlayer extends Player { this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; this.clock = clock; } @@ -251,6 +264,36 @@ public interface ExoPlayer extends Player { return this; } + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets whether media sources should be initialized lazily. + * + *

        If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + /** * Sets the {@link Clock} that will be used by the player. Should only be set for testing * purposes. 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 ba63dee80e..b5956c9dae 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 @@ -95,6 +95,7 @@ public class SimpleExoPlayer extends BasePlayer private BandwidthMeter bandwidthMeter; private AnalyticsCollector analyticsCollector; private Looper looper; + private boolean useLazyPreparation; private boolean buildCalled; /** @@ -115,6 +116,7 @@ public class SimpleExoPlayer extends BasePlayer * Looper} of the application's main thread if the current thread doesn't have a {@link * Looper} *

      • {@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT} + *
      • {@code useLazyPreparation}: {@code true} *
      • {@link Clock}: {@link Clock#DEFAULT} * * @@ -142,6 +144,7 @@ public class SimpleExoPlayer extends BasePlayer DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -160,6 +163,7 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. */ public Builder( @@ -170,6 +174,7 @@ public class SimpleExoPlayer extends BasePlayer BandwidthMeter bandwidthMeter, Looper looper, AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock) { this.context = context; this.renderersFactory = renderersFactory; @@ -178,6 +183,7 @@ public class SimpleExoPlayer extends BasePlayer this.bandwidthMeter = bandwidthMeter; this.looper = looper; this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; this.clock = clock; } @@ -247,6 +253,23 @@ public class SimpleExoPlayer extends BasePlayer return this; } + /** + * Sets whether media sources should be initialized lazily. + * + *

        If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + /** * Sets the {@link Clock} that will be used by the player. Should only be set for testing * purposes. From c361e3abc3cff2d63f16233547b48813c3d32c60 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 20 Aug 2019 08:40:44 +0100 Subject: [PATCH 346/807] Fix handling of delayed AdsLoader.start AdsMediaSource posts AdsLoader.start to the main thread during preparation, but the app may call AdsLoader.setPlayer(null) before it actually runs (e.g., if initializing then quickly backgrounding the player). This is valid usage of the API so handle this case instead of asserting. Because not calling setPlayer at all is a pitfall of the API, track whether setPlayer has been called and still assert that in AdsLoader.start. PiperOrigin-RevId: 264329632 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 e37f192c97..3a9d83769b 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 @@ -327,6 +327,7 @@ public final class ImaAdsLoader private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; private Object pendingAdRequestContext; private List supportedMimeTypes; @@ -558,6 +559,7 @@ public final class ImaAdsLoader Assertions.checkState( player == null || player.getApplicationLooper() == Looper.getMainLooper()); nextPlayer = player; + wasSetPlayerCalled = true; } @Override @@ -585,9 +587,12 @@ public final class ImaAdsLoader @Override public void start(EventListener eventListener, AdViewProvider adViewProvider) { - Assertions.checkNotNull( - nextPlayer, "Set player using adsLoader.setPlayer before preparing the player."); + Assertions.checkState( + wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player."); player = nextPlayer; + if (player == null) { + return; + } this.eventListener = eventListener; lastVolumePercentage = 0; lastAdProgress = null; @@ -617,6 +622,9 @@ public final class ImaAdsLoader @Override public void stop() { + if (player == null) { + return; + } if (adsManager != null && imaPausedContent) { adPlaybackState = adPlaybackState.withAdResumePositionUs( From f0aae7aee5db46c2386128cdd5fe49f31f3a5389 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 21 Aug 2019 12:47:45 +0100 Subject: [PATCH 347/807] Support out-of-band HDR10+ metadata for VP9 Extract supplemental data from block additions in WebM/Matroska. Allow storing supplemental data alongside samples in the SampleQueue and write it as a separate field in DecoderInputBuffers. Handle supplemental data in the VP9 extension by propagating it to the output buffer. Handle supplemental data for HDR10+ in MediaCodecVideoRenderer by passing it to MediaCodec.setParameters, if supported by the component. PiperOrigin-RevId: 264582805 --- RELEASENOTES.md | 1 + .../exoplayer2/ext/vp9/VpxDecoder.java | 5 +- .../java/com/google/android/exoplayer2/C.java | 3 + .../android/exoplayer2/decoder/Buffer.java | 5 ++ .../decoder/DecoderInputBuffer.java | 19 ++++++ .../extractor/mkv/MatroskaExtractor.java | 65 +++++++++++++++++++ .../exoplayer2/mediacodec/MediaCodecInfo.java | 12 ++++ .../mediacodec/MediaCodecRenderer.java | 20 +++++- .../exoplayer2/source/SampleQueue.java | 52 +++++++++++---- .../video/MediaCodecVideoRenderer.java | 42 ++++++++++++ .../video/VideoDecoderOutputBuffer.java | 21 +++++- 11 files changed, 230 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8415f57c9a..6b5f4a0002 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,7 @@ * Fix issue where player errors are thrown too early at playlist transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be set. +* Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. ### 2.10.4 ### 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 1392e782f8..462e6ea044 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 @@ -139,7 +139,10 @@ import java.nio.ByteBuffer; } if (!inputBuffer.isDecodeOnly()) { - outputBuffer.init(inputBuffer.timeUs, outputMode); + @Nullable + ByteBuffer supplementalData = + inputBuffer.hasSupplementalData() ? inputBuffer.supplementalData : null; + outputBuffer.init(inputBuffer.timeUs, outputMode, supplementalData); int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer); if (getFrameResult == 1) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); 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 cd862e503f..d073eb4ee0 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 @@ -480,6 +480,7 @@ public final class C { value = { BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA, BUFFER_FLAG_LAST_SAMPLE, BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY @@ -493,6 +494,8 @@ public final class C { * Flag for empty buffers that signal that the end of the stream was reached. */ public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; + /** Indicates that a buffer has supplemental data. */ + public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000 /** Indicates that a buffer is known to contain the last media sample of the stream. */ public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000 /** Indicates that a buffer is (at least partially) encrypted. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java index 773959fbfc..8fd25f2cf9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java @@ -53,6 +53,11 @@ public abstract class Buffer { return getFlag(C.BUFFER_FLAG_KEY_FRAME); } + /** Returns whether the {@link C#BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA} flag is set. */ + public final boolean hasSupplementalData() { + return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); + } + /** * Replaces this buffer's flags with {@code flags}. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index c31ae92cfc..7a19d85aa8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -68,6 +68,12 @@ public class DecoderInputBuffer extends Buffer { */ public long timeUs; + /** + * Supplemental data related to the buffer, if {@link #hasSupplementalData()} returns true. If + * present, the buffer is populated with supplemental data from position 0 to its limit. + */ + @Nullable public ByteBuffer supplementalData; + @BufferReplacementMode private final int bufferReplacementMode; /** @@ -89,6 +95,16 @@ public class DecoderInputBuffer extends Buffer { this.bufferReplacementMode = bufferReplacementMode; } + /** Resets {@link #supplementalData} in preparation for storing {@code length} bytes. */ + @EnsuresNonNull("supplementalData") + public void resetSupplementalData(int length) { + if (supplementalData == null || supplementalData.capacity() < length) { + supplementalData = ByteBuffer.allocate(length); + } + supplementalData.position(0); + supplementalData.limit(length); + } + /** * Ensures that {@link #data} is large enough to accommodate a write of a given length at its * current position. @@ -148,6 +164,9 @@ public class DecoderInputBuffer extends Buffer { */ public final void flip() { data.flip(); + if (supplementalData != null) { + supplementalData.flip(); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index c785865f6a..e4f42fcf91 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -149,6 +149,10 @@ public class MatroskaExtractor implements Extractor { private static final int ID_BLOCK_GROUP = 0xA0; private static final int ID_BLOCK = 0xA1; private static final int ID_BLOCK_DURATION = 0x9B; + private static final int ID_BLOCK_ADDITIONS = 0x75A1; + private static final int ID_BLOCK_MORE = 0xA6; + private static final int ID_BLOCK_ADD_ID = 0xEE; + private static final int ID_BLOCK_ADDITIONAL = 0xA5; private static final int ID_REFERENCE_BLOCK = 0xFB; private static final int ID_TRACKS = 0x1654AE6B; private static final int ID_TRACK_ENTRY = 0xAE; @@ -157,6 +161,7 @@ public class MatroskaExtractor implements Extractor { private static final int ID_FLAG_DEFAULT = 0x88; private static final int ID_FLAG_FORCED = 0x55AA; private static final int ID_DEFAULT_DURATION = 0x23E383; + private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE; private static final int ID_NAME = 0x536E; private static final int ID_CODEC_ID = 0x86; private static final int ID_CODEC_PRIVATE = 0x63A2; @@ -215,6 +220,12 @@ public class MatroskaExtractor implements Extractor { private static final int ID_LUMNINANCE_MAX = 0x55D9; private static final int ID_LUMNINANCE_MIN = 0x55DA; + /** + * BlockAddID value for ITU T.35 metadata in a VP9 track. See also + * https://www.webmproject.org/docs/container/. + */ + private static final int BLOCK_ADD_ID_VP9_ITU_T_35 = 4; + private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; private static final int LACING_FIXED_SIZE = 2; @@ -323,6 +334,7 @@ public class MatroskaExtractor implements Extractor { private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; + private final ParsableByteArray blockAddData; private ByteBuffer encryptionSubsampleDataBuffer; private long segmentContentSize; @@ -361,6 +373,7 @@ public class MatroskaExtractor implements Extractor { private int blockTrackNumberLength; @C.BufferFlags private int blockFlags; + private int blockAddId; // Sample reading state. private int sampleBytesRead; @@ -401,6 +414,7 @@ public class MatroskaExtractor implements Extractor { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); + blockAddData = new ParsableByteArray(); } @Override @@ -479,6 +493,8 @@ public class MatroskaExtractor implements Extractor { case ID_CUE_POINT: case ID_CUE_TRACK_POSITIONS: case ID_BLOCK_GROUP: + case ID_BLOCK_ADDITIONS: + case ID_BLOCK_MORE: case ID_PROJECTION: case ID_COLOUR: case ID_MASTERING_METADATA: @@ -499,6 +515,7 @@ public class MatroskaExtractor implements Extractor { case ID_FLAG_DEFAULT: case ID_FLAG_FORCED: case ID_DEFAULT_DURATION: + case ID_MAX_BLOCK_ADDITION_ID: case ID_CODEC_DELAY: case ID_SEEK_PRE_ROLL: case ID_CHANNELS: @@ -518,6 +535,7 @@ public class MatroskaExtractor implements Extractor { case ID_MAX_CLL: case ID_MAX_FALL: case ID_PROJECTION_TYPE: + case ID_BLOCK_ADD_ID: return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT; case ID_DOC_TYPE: case ID_NAME: @@ -531,6 +549,7 @@ public class MatroskaExtractor implements Extractor { case ID_BLOCK: case ID_CODEC_PRIVATE: case ID_PROJECTION_PRIVATE: + case ID_BLOCK_ADDITIONAL: return EbmlProcessor.ELEMENT_TYPE_BINARY; case ID_DURATION: case ID_SAMPLING_FREQUENCY: @@ -760,6 +779,9 @@ public class MatroskaExtractor implements Extractor { case ID_DEFAULT_DURATION: currentTrack.defaultSampleDurationNs = (int) value; break; + case ID_MAX_BLOCK_ADDITION_ID: + currentTrack.maxBlockAdditionId = (int) value; + break; case ID_CODEC_DELAY: currentTrack.codecDelayNs = value; break; @@ -914,6 +936,9 @@ public class MatroskaExtractor implements Extractor { break; } break; + case ID_BLOCK_ADD_ID: + blockAddId = (int) value; + break; default: break; } @@ -1171,12 +1196,30 @@ public class MatroskaExtractor implements Extractor { writeSampleData(input, track, blockLacingSampleSizes[0]); } + break; + case ID_BLOCK_ADDITIONAL: + if (blockState != BLOCK_STATE_DATA) { + return; + } + handleBlockAdditionalData(tracks.get(blockTrackNumber), blockAddId, input, contentSize); break; default: throw new ParserException("Unexpected id: " + id); } } + protected void handleBlockAdditionalData( + Track track, int blockAddId, ExtractorInput input, int contentSize) + throws IOException, InterruptedException { + if (blockAddId == BLOCK_ADD_ID_VP9_ITU_T_35 && CODEC_ID_VP9.equals(track.codecId)) { + blockAddData.reset(contentSize); + input.readFully(blockAddData.data, 0, contentSize); + } else { + // Unhandled block additional data. + input.skipFully(contentSize); + } + } + private void commitSampleToOutput(Track track, long timeUs) { if (track.trueHdSampleRechunker != null) { track.trueHdSampleRechunker.sampleMetadata(track, timeUs); @@ -1196,6 +1239,12 @@ public class MatroskaExtractor implements Extractor { SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY); } + if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + // Append supplemental data. + int size = blockAddData.limit(); + track.output.sampleData(blockAddData, size); + sampleBytesWritten += size; + } track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); } sampleRead = true; @@ -1328,6 +1377,21 @@ public class MatroskaExtractor implements Extractor { // If the sample has header stripping, prepare to read/output the stripped bytes first. sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length); } + + if (track.maxBlockAdditionId > 0) { + blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + blockAddData.reset(); + // If there is supplemental data, the structure of the sample data is: + // sample size (4 bytes) || sample data || supplemental data + scratch.reset(/* limit= */ 4); + scratch.data[0] = (byte) ((size >> 24) & 0xFF); + scratch.data[1] = (byte) ((size >> 16) & 0xFF); + scratch.data[2] = (byte) ((size >> 8) & 0xFF); + scratch.data[3] = (byte) (size & 0xFF); + output.sampleData(scratch, 4); + sampleBytesWritten += 4; + } + sampleEncodingHandled = true; } size += sampleStrippedBytes.limit(); @@ -1713,6 +1777,7 @@ public class MatroskaExtractor implements Extractor { public int number; public int type; public int defaultSampleDurationNs; + public int maxBlockAdditionId; public boolean hasContentEncryption; public byte[] sampleStrippedBytes; public TrackOutput.CryptoData cryptoData; 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 d07def1894..c700259b13 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 @@ -264,6 +264,18 @@ public final class MediaCodecInfo { return false; } + /** Whether the codec handles HDR10+ out-of-band metadata. */ + public boolean isHdr10PlusOutOfBandMetadataSupported() { + if (Util.SDK_INT >= 29 && MimeTypes.VIDEO_VP9.equals(mimeType)) { + for (CodecProfileLevel capabilities : getProfileLevels()) { + if (capabilities.profile == CodecProfileLevel.VP9Profile2HDR10Plus) { + return true; + } + } + } + return false; + } + /** * Returns whether it may be possible to adapt to playing a different format when the codec is * configured to play media in the specified {@code format}. For adaptation to succeed, the codec 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 ee2c9ad1a3..c077d8d227 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 @@ -1140,6 +1140,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); + if (buffer.hasSupplementalData()) { + handleInputBufferSupplementalData(buffer); + } onQueueInputBuffer(buffer); if (bufferEncrypted) { @@ -1297,10 +1300,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Do nothing. } + /** + * Handles supplemental data associated with an input buffer. + * + *

        The default implementation is a no-op. + * + * @param buffer The input buffer that is about to be queued. + * @throws ExoPlaybackException Thrown if an error occurs handling supplemental data. + */ + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) + throws ExoPlaybackException { + // Do nothing. + } + /** * Called immediately before an input buffer is queued into the codec. - *

        - * The default implementation is a no-op. + * + *

        The default implementation is a no-op. * * @param buffer The buffer to be queued. */ 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 921afcdf2f..fa4a26aa3c 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 @@ -393,13 +393,7 @@ public class SampleQueue implements TrackOutput { buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } if (!buffer.isFlagsOnly()) { - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); - } - // Write the sample data into the holder. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); + readToBuffer(buffer, extrasHolder); } } return C.RESULT_BUFFER_READ; @@ -410,12 +404,48 @@ public class SampleQueue implements TrackOutput { } } + /** + * Reads data from the rolling buffer to populate a decoder input buffer. + * + * @param buffer The buffer to populate. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Read sample data, extracting supplemental data into a separate buffer if needed. + if (buffer.hasSupplementalData()) { + // If there is supplemental data, the sample data is prefixed by its size. + scratch.reset(4); + readData(extrasHolder.offset, scratch.data, 4); + int sampleSize = scratch.readUnsignedIntToInt(); + extrasHolder.offset += 4; + extrasHolder.size -= 4; + + // Write the sample data. + buffer.ensureSpaceForWrite(sampleSize); + readData(extrasHolder.offset, buffer.data, sampleSize); + extrasHolder.offset += sampleSize; + extrasHolder.size -= sampleSize; + + // Write the remaining data as supplemental data. + buffer.resetSupplementalData(extrasHolder.size); + readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); + } else { + // Write the sample data. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + } + } + /** * Reads encryption data for the current sample. - *

        - * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and - * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The - * same value is added to {@link SampleExtrasHolder#offset}. + * + *

        The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link + * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same + * value is added to {@link SampleExtrasHolder#offset}. * * @param buffer The buffer into which the encryption data should be written. * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. 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 de77e8318d..0310800876 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 @@ -23,6 +23,7 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCrypto; import android.media.MediaFormat; +import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import androidx.annotation.CallSuper; @@ -123,6 +124,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private CodecMaxValues codecMaxValues; private boolean codecNeedsSetOutputSurfaceWorkaround; + private boolean codecHandlesHdr10PlusOutOfBandMetadata; private Surface surface; private Surface dummySurface; @@ -683,6 +685,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); + codecHandlesHdr10PlusOutOfBandMetadata = + Assertions.checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported(); } @Override @@ -727,6 +731,37 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { processOutputFormat(codec, width, height); } + @Override + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) + throws ExoPlaybackException { + if (!codecHandlesHdr10PlusOutOfBandMetadata) { + return; + } + ByteBuffer data = Assertions.checkNotNull(buffer.supplementalData); + if (data.remaining() >= 7) { + // Check for HDR10+ out-of-band metadata. See User_data_registered_itu_t_t35 in ST 2094-40. + byte ituTT35CountryCode = data.get(); + int ituTT35TerminalProviderCode = data.getShort(); + int ituTT35TerminalProviderOrientedCode = data.getShort(); + byte applicationIdentifier = data.get(); + byte applicationVersion = data.get(); + data.position(0); + if (ituTT35CountryCode == (byte) 0xB5 + && ituTT35TerminalProviderCode == 0x003C + && ituTT35TerminalProviderOrientedCode == 0x0001 + && applicationIdentifier == 4 + && applicationVersion == 0) { + // The metadata size may vary so allocate a new array every time. This is not too + // inefficient because the metadata is only a few tens of bytes. + byte[] hdr10PlusInfo = new byte[data.remaining()]; + data.get(hdr10PlusInfo); + data.position(0); + // If codecHandlesHdr10PlusOutOfBandMetadata is true, this is an API 29 or later build. + setHdr10PlusInfoV29(getCodec(), hdr10PlusInfo); + } + } + } + @Override protected boolean processOutputBuffer( long positionUs, @@ -1153,6 +1188,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -500000; } + @TargetApi(29) + private static void setHdr10PlusInfoV29(MediaCodec codec, byte[] hdr10PlusInfo) { + Bundle codecParameters = new Bundle(); + codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10PlusInfo); + codec.setParameters(codecParameters); + } + @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); 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 index b4b09b20a2..10ccb4eba2 100644 --- 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 @@ -46,16 +46,35 @@ public abstract class VideoDecoderOutputBuffer extends OutputBuffer { @Nullable public int[] yuvStrides; public int colorspace; + /** + * Supplemental data related to the output frame, if {@link #hasSupplementalData()} returns true. + * If present, the buffer is populated with supplemental data from position 0 to its limit. + */ + @Nullable public ByteBuffer supplementalData; + /** * 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}. + * @param supplementalData Supplemental data associated with the frame, or {@code null} if not + * present. It is safe to reuse the provided buffer after this method returns. */ - public void init(long timeUs, @C.VideoOutputMode int mode) { + public void init( + long timeUs, @C.VideoOutputMode int mode, @Nullable ByteBuffer supplementalData) { this.timeUs = timeUs; this.mode = mode; + if (supplementalData != null) { + int size = supplementalData.limit(); + if (this.supplementalData == null || this.supplementalData.capacity() < size) { + this.supplementalData = ByteBuffer.allocate(size); + } + this.supplementalData.position(0); + this.supplementalData.put(supplementalData); + this.supplementalData.flip(); + supplementalData.position(0); + } } /** From 6a1331f125a2e4a501bdb0be4d443cf014c97502 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 21 Aug 2019 14:59:05 +0100 Subject: [PATCH 348/807] Gracefully handle chunkful preparation without chunks. This situation happens if the first chunk to load is already behind the end of the stream. In this case, the preparation never completes because HlsSampleStreamWrapper gets stuck in a prepared=false and loadingFinished=true state. Gracefully handle this situation by attempting to load the last chunk if still unprepared to ensure that track information is obtained as far as possible. Otherwise, it wouldn't be possible to play anything even when seeking back. Issue:#6314 PiperOrigin-RevId: 264599465 --- RELEASENOTES.md | 2 ++ .../exoplayer2/source/hls/HlsChunkSource.java | 20 +++++++++++++++---- .../source/hls/HlsSampleStreamWrapper.java | 11 +++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6b5f4a0002..dca6e13cc9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -42,6 +42,8 @@ ([#5407](https://github.com/google/ExoPlayer/issues/5407)). * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be set. * Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. +* Fix issue where HLS streams get stuck in infinite buffering state after + postroll ad ([#6314](https://github.com/google/ExoPlayer/issues/6314)). ### 2.10.4 ### 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 c452a29cf9..370d79edc7 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 @@ -225,10 +225,17 @@ import java.util.Map; * media in previous periods still to be played. * @param loadPositionUs The current load position relative to the period start in microseconds. * @param queue The queue of buffered {@link HlsMediaChunk}s. + * @param allowEndOfStream Whether {@link HlsChunkHolder#endOfStream} is allowed to be set for + * non-empty media playlists. If {@code false}, the last available chunk is returned instead. + * If the media playlist is empty, {@link HlsChunkHolder#endOfStream} is always set. * @param out A holder to populate. */ public void getNextChunk( - long playbackPositionUs, long loadPositionUs, List queue, HlsChunkHolder out) { + long playbackPositionUs, + long loadPositionUs, + List queue, + boolean allowEndOfStream, + HlsChunkHolder out) { HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); long bufferedDurationUs = loadPositionUs - playbackPositionUs; @@ -292,15 +299,20 @@ import java.util.Map; } int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence); - if (segmentIndexInPlaylist >= mediaPlaylist.segments.size()) { + int availableSegmentCount = mediaPlaylist.segments.size(); + if (segmentIndexInPlaylist >= availableSegmentCount) { if (mediaPlaylist.hasEndTag) { - out.endOfStream = true; + if (allowEndOfStream || availableSegmentCount == 0) { + out.endOfStream = true; + return; + } + segmentIndexInPlaylist = availableSegmentCount - 1; } else /* Live */ { out.playlistUrl = selectedPlaylistUrl; seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); expectedPlaylistUrl = selectedPlaylistUrl; + return; } - return; } // We have a valid playlist snapshot, we can discard any playlist errors at this point. seenExpectedPlaylistError = false; 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 ff725ec6f7..e4c756c6b6 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 @@ -21,6 +21,7 @@ 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.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; @@ -232,6 +233,9 @@ import java.util.Set; public void maybeThrowPrepareError() throws IOException { maybeThrowError(); + if (loadingFinished && !prepared) { + throw new ParserException("Loading finished before preparation is complete."); + } } public TrackGroupArray getTrackGroups() { @@ -608,7 +612,12 @@ import java.util.Set; ? lastMediaChunk.endTimeUs : Math.max(lastSeekPositionUs, lastMediaChunk.startTimeUs); } - chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); + chunkSource.getNextChunk( + positionUs, + loadPositionUs, + chunkQueue, + /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(), + nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; From 51476fa2c8d600490e41677f44ca309e2092741b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 21 Aug 2019 15:22:47 +0100 Subject: [PATCH 349/807] Prevent NPE in ImaAdsLoader onPositionDiscontinuity. Any seek before the first timeline becomes available will result in a NPE. Change it to handle that case gracefully. Issue:#5831 PiperOrigin-RevId: 264603061 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 3a9d83769b..f1a4036038 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 @@ -484,6 +484,7 @@ public final class ImaAdsLoader pendingContentPositionMs = C.TIME_UNSET; adGroupIndex = C.INDEX_UNSET; contentDurationMs = C.TIME_UNSET; + timeline = Timeline.EMPTY; } /** @@ -966,7 +967,7 @@ public final class ImaAdsLoader if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } - updateImaStateForPlayerState(); + onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @Override @@ -1021,7 +1022,7 @@ public final class ImaAdsLoader } } updateAdPlaybackState(); - } else { + } else if (!timeline.isEmpty()) { long positionMs = player.getCurrentPosition(); timeline.getPeriod(0, period); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); @@ -1033,9 +1034,8 @@ public final class ImaAdsLoader } } } - } else { - updateImaStateForPlayerState(); } + updateImaStateForPlayerState(); } // Internal methods. From 6748eeca6afc6273a6a1df760995ce89c3680c54 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 22 Aug 2019 08:44:39 +0100 Subject: [PATCH 350/807] Avoid potential ArrayStoreException with audio processors The app is able to pass a more specialized array type, so the Arrays.copyOf call produces an array into which it's not valid to store arbitrary AudioProcessors. Create a new array and copy into it to avoid this problem. PiperOrigin-RevId: 264779164 --- .../android/exoplayer2/audio/DefaultAudioSink.java | 11 +++++++++-- .../exoplayer2/audio/DefaultAudioSinkTest.java | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) 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 b4e0058982..65d997396b 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 @@ -37,7 +37,6 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; /** @@ -122,7 +121,15 @@ public final class DefaultAudioSink implements AudioSink { * audioProcessors} applied before silence skipping and playback parameters. */ public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { - this.audioProcessors = Arrays.copyOf(audioProcessors, audioProcessors.length + 2); + // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array + // rather than using Arrays.copyOf. + this.audioProcessors = new AudioProcessor[audioProcessors.length + 2]; + System.arraycopy( + /* src= */ audioProcessors, + /* srcPos= */ 0, + /* dest= */ this.audioProcessors, + /* destPos= */ 0, + /* length= */ audioProcessors.length); silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; 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 c0b5205455..7982163ee8 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 @@ -67,6 +67,13 @@ public final class DefaultAudioSinkTest { /* enableConvertHighResIntPcmToFloat= */ false); } + @Test + public void handlesSpecializedAudioProcessorArray() { + defaultAudioSink = + new DefaultAudioSink( + AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES, new TeeAudioProcessor[0]); + } + @Test public void handlesBufferAfterReset() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); From 7883eabb38afb53a53500d15dc6b97d81437b333 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 23 Aug 2019 09:57:26 +0100 Subject: [PATCH 351/807] Update comment to indicate correct int value of "FLAG_ALLOW_CACHE_FRAGMENTATION" in ExoPlayer2 upstream DataSpec Currently the value of FLAG_ALLOW_CACHE_FRAGMENTATION is defined as "1 << 4" but commented as "8". Either the value of FLAG_ALLOW_CACHE_FRAGMENTATION should be "1 << 3", or the comment should be 16. Here I am modifying the comment since it does not affect any current behavior. PiperOrigin-RevId: 265011839 --- .../java/com/google/android/exoplayer2/upstream/DataSpec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c2007b19a3..6e7b19936f 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 @@ -68,7 +68,7 @@ public final class DataSpec { * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment * whilst writing another). */ - public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 4; // 8 + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 4; // 16 /** * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link From 29af6899feebdb4e348cd50ea5eb595af059771a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 23 Aug 2019 10:18:26 +0100 Subject: [PATCH 352/807] Move playback error into PlaybackInfo. The error is closely related to the playback state IDLE and should be updated in sync with the state to prevent unexpected event ordering and/or keeping the error after re-preparation. Issue:#5407 PiperOrigin-RevId: 265014630 --- .../android/exoplayer2/ExoPlayerImpl.java | 33 ++++++++++------- .../exoplayer2/ExoPlayerImplInternal.java | 29 +++++++++------ .../android/exoplayer2/PlaybackInfo.java | 36 +++++++++++++++++++ .../analytics/AnalyticsCollector.java | 5 +-- .../exoplayer2/MediaPeriodQueueTest.java | 1 + 5 files changed, 77 insertions(+), 27 deletions(-) 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 cacdaec02e..38d66e5cbc 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 @@ -74,7 +74,6 @@ import java.util.concurrent.CopyOnWriteArrayList; private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; - @Nullable private ExoPlaybackException playbackError; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -202,13 +201,12 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override @Nullable public ExoPlaybackException getPlaybackError() { - return playbackError; + return playbackInfo.playbackError; } @Override public void retry() { - if (mediaSource != null - && (playbackError != null || playbackInfo.playbackState == Player.STATE_IDLE)) { + if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } } @@ -220,11 +218,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - playbackError = null; this.mediaSource = mediaSource; PlaybackInfo playbackInfo = getResetPlaybackInfo( - resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); + resetPosition, + resetState, + /* resetError= */ true, + /* playbackState= */ Player.STATE_BUFFERING); // Trigger internal prepare first before updating the playback info and notifying external // listeners to ensure that new operations issued in the listener notifications reach the // player after this prepare. The internal player can't change the playback info immediately @@ -381,13 +381,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void stop(boolean reset) { if (reset) { - playbackError = null; mediaSource = null; } PlaybackInfo playbackInfo = getResetPlaybackInfo( /* resetPosition= */ reset, /* resetState= */ reset, + /* resetError= */ reset, /* playbackState= */ Player.STATE_IDLE); // Trigger internal stop first before updating the playback info and notifying external // listeners to ensure that new operations issued in the listener notifications reach the @@ -415,6 +415,7 @@ import java.util.concurrent.CopyOnWriteArrayList; getResetPlaybackInfo( /* resetPosition= */ false, /* resetState= */ false, + /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } @@ -572,11 +573,6 @@ import java.util.concurrent.CopyOnWriteArrayList; case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0); break; - case ExoPlayerImplInternal.MSG_ERROR: - ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj; - this.playbackError = playbackError; - notifyListeners(listener -> listener.onPlayerError(playbackError)); - break; default: throw new IllegalStateException(); } @@ -635,7 +631,10 @@ import java.util.concurrent.CopyOnWriteArrayList; } private PlaybackInfo getResetPlaybackInfo( - boolean resetPosition, boolean resetState, @Player.State int playbackState) { + boolean resetPosition, + boolean resetState, + boolean resetError, + @Player.State int playbackState) { if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; @@ -659,6 +658,7 @@ import java.util.concurrent.CopyOnWriteArrayList; startPositionUs, contentPositionUs, playbackState, + resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, @@ -728,6 +728,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final @Player.TimelineChangeReason int timelineChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; + private final boolean playbackErrorChanged; private final boolean timelineChanged; private final boolean isLoadingChanged; private final boolean trackSelectorResultChanged; @@ -752,6 +753,9 @@ import java.util.concurrent.CopyOnWriteArrayList; this.seekProcessed = seekProcessed; this.playWhenReady = playWhenReady; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; + playbackErrorChanged = + previousPlaybackInfo.playbackError != playbackInfo.playbackError + && playbackInfo.playbackError != null; timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; trackSelectorResultChanged = @@ -770,6 +774,9 @@ import java.util.concurrent.CopyOnWriteArrayList; listenerSnapshot, listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason)); } + if (playbackErrorChanged) { + invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError)); + } if (trackSelectorResultChanged) { trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); 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 53c381961e..a6e8c679a8 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 @@ -61,7 +61,6 @@ import java.util.concurrent.atomic.AtomicBoolean; // External messages public static final int MSG_PLAYBACK_INFO_CHANGED = 0; public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; - public static final int MSG_ERROR = 2; // Internal messages private static final int MSG_PREPARE = 0; @@ -374,19 +373,19 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { Log.e(TAG, "Playback error.", e); - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(e); maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { Log.e(TAG, "Source error.", e); - eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); stopInternal( /* forceResetRenderers= */ false, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException | OutOfMemoryError e) { Log.e(TAG, "Internal runtime error.", e); @@ -394,11 +393,11 @@ import java.util.concurrent.atomic.AtomicBoolean; e instanceof OutOfMemoryError ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) : ExoPlaybackException.createForUnexpected((RuntimeException) e); - eventHandler.obtainMessage(MSG_ERROR, error).sendToTarget(); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(error); maybeNotifyPlaybackInfoChanged(); } return true; @@ -436,7 +435,11 @@ import java.util.concurrent.atomic.AtomicBoolean; private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { pendingPrepareCount++; resetInternal( - /* resetRenderers= */ false, /* releaseMediaSource= */ true, resetPosition, resetState); + /* resetRenderers= */ false, + /* releaseMediaSource= */ true, + resetPosition, + resetState, + /* resetError= */ true); loadControl.onPrepared(); this.mediaSource = mediaSource; setState(Player.STATE_BUFFERING); @@ -688,7 +691,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ false, /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false); + /* resetState= */ false, + /* resetError= */ true); } else { // Execute the seek in the current media periods. long newPeriodPositionUs = periodPositionUs; @@ -834,7 +838,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ forceResetRenderers || !foregroundMode, /* releaseMediaSource= */ true, /* resetPosition= */ resetPositionAndState, - /* resetState= */ resetPositionAndState); + /* resetState= */ resetPositionAndState, + /* resetError= */ resetPositionAndState); playbackInfoUpdate.incrementPendingOperationAcks( pendingPrepareCount + (acknowledgeStop ? 1 : 0)); pendingPrepareCount = 0; @@ -847,7 +852,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ true, /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* resetState= */ true); + /* resetState= */ true, + /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -861,7 +867,8 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean resetRenderers, boolean releaseMediaSource, boolean resetPosition, - boolean resetState) { + boolean resetState, + boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -924,6 +931,7 @@ import java.util.concurrent.atomic.AtomicBoolean; startPositionUs, contentPositionUs, playbackInfo.playbackState, + resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, @@ -1382,7 +1390,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ false, /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false); + /* resetState= */ false, + /* resetError= */ true); } /** 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 e9b99acd77..9d2a3b5459 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,6 +16,7 @@ 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; @@ -51,6 +52,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ @Player.State public final int playbackState; + /** The current playback error, or null if this is not an error state. */ + @Nullable public final ExoPlaybackException playbackError; /** Whether the player is currently loading. */ public final boolean isLoading; /** The currently available track groups. */ @@ -93,6 +96,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, Player.STATE_IDLE, + /* playbackError= */ null, /* isLoading= */ false, TrackGroupArray.EMPTY, emptyTrackSelectorResult, @@ -124,6 +128,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long startPositionUs, long contentPositionUs, @Player.State int playbackState, + @Nullable ExoPlaybackException playbackError, boolean isLoading, TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult, @@ -136,6 +141,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; this.playbackState = playbackState; + this.playbackError = playbackError; this.isLoading = isLoading; this.trackGroups = trackGroups; this.trackSelectorResult = trackSelectorResult; @@ -194,6 +200,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; positionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -217,6 +224,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -240,6 +248,31 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs, contentPositionUs, playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with a playback error. + * + * @param playbackError The error. See {@link #playbackError}. + * @return Copied playback info with the playback error. + */ + @CheckResult + public PlaybackInfo copyWithPlaybackError(@Nullable ExoPlaybackException playbackError) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -263,6 +296,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -288,6 +322,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -311,6 +346,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, 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 825424ae04..1ccd4ffc84 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 @@ -465,10 +465,7 @@ public class AnalyticsCollector @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = - error.type == ExoPlaybackException.TYPE_SOURCE - ? generateLoadingMediaPeriodEventTime() - : generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } 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 3c6c4462ca..afcce904e9 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 @@ -359,6 +359,7 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, /* contentPositionUs= */ 0, Player.STATE_READY, + /* playbackError= */ null, /* isLoading= */ false, /* trackGroups= */ null, /* trackSelectorResult= */ null, From aae64151e041c0361bd5c8b71e6aba6e4c1bc5dc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 23 Aug 2019 10:42:23 +0100 Subject: [PATCH 353/807] Add @CallSuper annotations in SimpleDecoder The implementation can't work properly unless these methods are called by subclasses, so annotate them to require calling the super implementation when overriding. PiperOrigin-RevId: 265017433 --- .../com/google/android/exoplayer2/decoder/SimpleDecoder.java | 3 +++ 1 file changed, 3 insertions(+) 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 b7465f82eb..03aabecb0e 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.decoder; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; @@ -125,6 +126,7 @@ public abstract class SimpleDecoder< * * @param outputBuffer The output buffer being released. */ + @CallSuper protected void releaseOutputBuffer(O outputBuffer) { synchronized (lock) { releaseOutputBufferInternal(outputBuffer); @@ -150,6 +152,7 @@ public abstract class SimpleDecoder< } } + @CallSuper @Override public void release() { synchronized (lock) { From 75f744ae4077baa10ab3e1170ed330319739905c Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Fri, 23 Aug 2019 10:53:03 +0100 Subject: [PATCH 354/807] Fix VpxDecoder error codes to match the ones in vpx_jni.cc PiperOrigin-RevId: 265018783 --- .../com/google/android/exoplayer2/ext/vp9/VpxDecoder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 462e6ea044..5630058712 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 @@ -30,9 +30,11 @@ import java.nio.ByteBuffer; /* package */ final class VpxDecoder extends SimpleDecoder { + // These constants should match the codes returned from vpxDecode and vpxSecureDecode functions in + // https://github.com/google/ExoPlayer/blob/release-v2/extensions/vp9/src/main/jni/vpx_jni.cc. private static final int NO_ERROR = 0; - private static final int DECODE_ERROR = 1; - private static final int DRM_ERROR = 2; + private static final int DECODE_ERROR = -1; + private static final int DRM_ERROR = -2; @Nullable private final ExoMediaCrypto exoMediaCrypto; private final long vpxDecContext; From 34cc5f4cb8841930da6d2498370904cbea0d8026 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 23 Aug 2019 16:16:07 +0100 Subject: [PATCH 355/807] Defer adsManager.init until the timeline has loaded If the app seeks after we get an ads manager but before the player exposes the timeline with ads, we would end up expecting to play a preroll even after the seek request arrived. This caused the player to get stuck. Wait until a non-empty timeline has been exposed via onTimelineChanged before initializing IMA (at which point it can start polling the player position). Seek requests are not handled while an ad is playing. PiperOrigin-RevId: 265058325 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 20 +++++++++++-------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 3 ++- 2 files changed, 14 insertions(+), 9 deletions(-) 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 f1a4036038..3b2bcd8880 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 @@ -338,6 +338,7 @@ public final class ImaAdsLoader private int lastVolumePercentage; private AdsManager adsManager; + private boolean initializedAdsManager; private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; @@ -613,8 +614,8 @@ public final class ImaAdsLoader adsManager.resume(); } } else if (adsManager != null) { - // Ads have loaded but the ads manager is not initialized. - startAdPlayback(); + adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); + updateAdPlaybackState(); } else { // Ads haven't loaded yet, so request them. requestAds(adViewGroup); @@ -688,7 +689,8 @@ public final class ImaAdsLoader if (player != null) { // If a player is attached already, start playback immediately. try { - startAdPlayback(); + adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); + updateAdPlaybackState(); } catch (Exception e) { maybeNotifyInternalError("onAdsManagerLoaded", e); } @@ -967,6 +969,10 @@ public final class ImaAdsLoader if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } + if (!initializedAdsManager && adsManager != null) { + initializedAdsManager = true; + initializeAdsManager(); + } onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @@ -1040,7 +1046,7 @@ public final class ImaAdsLoader // Internal methods. - private void startAdPlayback() { + private void initializeAdsManager() { AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); @@ -1055,10 +1061,9 @@ public final class ImaAdsLoader adsRenderingSettings.setUiElements(adUiElements); } - // Set up the ad playback state, skipping ads based on the start position as required. + // Skip ads based on the start position as required. long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); - long contentPositionMs = player.getCurrentPosition(); + long contentPositionMs = player.getContentPosition(); int adGroupIndexForPosition = adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { @@ -1092,7 +1097,6 @@ public final class ImaAdsLoader pendingContentPositionMs = contentPositionMs; } - // Start ad playback. adsManager.init(adsRenderingSettings); updateAdPlaybackState(); if (DEBUG) { 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 ab880703ee..98aee17d73 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 @@ -143,7 +143,8 @@ public class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( new AdPlaybackState(/* adGroupTimesUs= */ 0) - .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)); + .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) + .withContentDurationUs(CONTENT_DURATION_US)); } @Test From d1084f27284d44e8ad1652e33df9f1fae335ad9e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 23 Aug 2019 16:57:14 +0100 Subject: [PATCH 356/807] Do not compare bitrates of audio tracks with different languages. The last selection criteria is the audio bitrate to prefer higher-quality streams. We shouldn't apply this criterium though if the languages of the tracks are different. Issue:#6335 PiperOrigin-RevId: 265064756 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 8 +- .../DefaultTrackSelectorTest.java | 91 ++++++++++++++----- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dca6e13cc9..5f7d5bea9c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,8 @@ * Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. * Fix issue where HLS streams get stuck in infinite buffering state after postroll ad ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Fix audio selection issue where languages are compared by bit rate + ([#6335](https://github.com/google/ExoPlayer/issues/6335)). ### 2.10.4 ### 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 b43701f1bc..21dff0b4b2 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 @@ -2548,6 +2548,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean isWithinConstraints; + @Nullable private final String language; private final Parameters parameters; private final boolean isWithinRendererCapabilities; private final int preferredLanguageScore; @@ -2560,6 +2561,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { this.parameters = parameters; + this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); preferredLanguageScore = getFormatLanguageScore( @@ -2633,7 +2635,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (this.sampleRate != other.sampleRate) { return resultSign * compareInts(this.sampleRate, other.sampleRate); } - return resultSign * compareInts(this.bitrate, other.bitrate); + if (Util.areEqual(this.language, other.language)) { + // Only compare bit rates of tracks with the same or unknown language. + return resultSign * compareInts(this.bitrate, other.bitrate); + } + return 0; } } 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 0374f88bae..7c57c44924 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 @@ -717,37 +717,38 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will select audio tracks with higher bit-rate when other factors are - * the same, and tracks are within renderer's capabilities. + * Tests that track selector will select audio tracks with higher bit rate when other factors are + * the same, and tracks are within renderer's capabilities, and have the same language. */ @Test - public void testSelectTracksWithinCapabilitiesSelectHigherBitrate() throws Exception { + public void selectAudioTracks_withinCapabilities_andSameLanguage_selectsHigherBitrate() + throws Exception { Format lowerBitrateFormat = Format.createAudioSampleFormat( "audioFormat", MimeTypes.AUDIO_AAC, - null, - 15000, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - null); + /* codecs= */ null, + /* bitrate= */ 15000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "hi"); Format higherBitrateFormat = Format.createAudioSampleFormat( "audioFormat", MimeTypes.AUDIO_AAC, - null, - 30000, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - null); + /* codecs= */ null, + /* bitrate= */ 30000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "hi"); TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat); TrackSelectorResult result = @@ -759,14 +760,58 @@ public final class DefaultTrackSelectorTest { assertFixedSelection(result.selections.get(0), trackGroups, higherBitrateFormat); } + /** + * Tests that track selector will select the first audio track even if other tracks with a + * different language have higher bit rates, all other factors are the same, and tracks are within + * renderer's capabilities. + */ + @Test + public void selectAudioTracks_withinCapabilities_andDifferentLanguage_selectsFirstTrack() + throws Exception { + Format firstLanguageFormat = + Format.createAudioSampleFormat( + "audioFormat", + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + /* bitrate= */ 15000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "hi"); + Format higherBitrateFormat = + Format.createAudioSampleFormat( + "audioFormat", + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + /* bitrate= */ 30000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ "te"); + TrackGroupArray trackGroups = wrapFormats(firstLanguageFormat, higherBitrateFormat); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + trackGroups, + periodId, + TIMELINE); + assertFixedSelection(result.selections.get(0), trackGroups, firstLanguageFormat); + } + /** * Tests that track selector will prefer audio tracks with higher channel count over tracks with * higher sample rate when other factors are the same, and tracks are within renderer's * capabilities. */ @Test - public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() - throws Exception { + public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception { Format higherChannelLowerSampleRateFormat = Format.createAudioSampleFormat( "audioFormat", From ce37b7eea20c7af2db20486a604d8985d13f7233 Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 23 Aug 2019 18:48:14 +0100 Subject: [PATCH 357/807] Add HTTP request parameters (headers) to DataSpec. Adds HTTP request parameters in DataSpec. Keeps DataSpec behavior to be immutable as before. PiperOrigin-RevId: 265087782 --- .../android/exoplayer2/upstream/DataSpec.java | 60 +++++++- .../exoplayer2/upstream/DataSpecTest.java | 128 ++++++++++++++++++ 2 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java 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 6e7b19936f..f297bb468e 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 @@ -24,6 +24,9 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * Defines a region of data. @@ -100,9 +103,10 @@ public final class DataSpec { */ @Nullable public final byte[] httpBody; - /** - * The absolute position of the data in the full stream. - */ + /** Immutable map containing the headers to use in HTTP requests. */ + public final Map httpRequestHeaders; + + /** The absolute position of the data in the full stream. */ public final long absoluteStreamPosition; /** * The position of the data when read from {@link #uri}. @@ -233,7 +237,6 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - @SuppressWarnings("deprecation") public DataSpec( Uri uri, @HttpMethod int httpMethod, @@ -243,6 +246,41 @@ public final class DataSpec { long length, @Nullable String key, @Flags int flags) { + this( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + /* httpRequestHeaders= */ Collections.emptyMap()); + } + + /** + * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); @@ -254,6 +292,7 @@ public final class DataSpec { this.length = length; this.key = key; this.flags = flags; + this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); } /** @@ -341,7 +380,8 @@ public final class DataSpec { position + offset, length, key, - flags); + flags, + httpRequestHeaders); } } @@ -353,6 +393,14 @@ public final class DataSpec { */ public DataSpec withUri(Uri uri) { return new DataSpec( - uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags); + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + httpRequestHeaders); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java new file mode 100644 index 0000000000..f6e30f814a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java @@ -0,0 +1,128 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link DataSpec}. */ +@RunWith(AndroidJUnit4.class) +public class DataSpecTest { + + @Test + public void createDataSpec_withDefaultValues_setsEmptyHttpRequestParameters() { + Uri uri = Uri.parse("www.google.com"); + DataSpec dataSpec = new DataSpec(uri); + + assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); + + dataSpec = new DataSpec(uri, /*flags= */ 0); + assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); + + dataSpec = + new DataSpec( + uri, + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0); + assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue(); + } + + @Test + public void createDataSpec_setsHttpRequestParameters() { + Map httpRequestParameters = new HashMap<>(); + httpRequestParameters.put("key1", "value1"); + httpRequestParameters.put("key2", "value2"); + httpRequestParameters.put("key3", "value3"); + + DataSpec dataSpec = + new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + httpRequestParameters); + + assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestParameters); + } + + @Test + public void httpRequestParameters_areReadOnly() { + DataSpec dataSpec = + new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + /* httpRequestHeaders= */ new HashMap<>()); + + try { + dataSpec.httpRequestHeaders.put("key", "value"); + fail(); + } catch (UnsupportedOperationException expected) { + // Expected + } + } + + @Test + public void copyMethods_copiesHttpRequestHeaders() { + Map httpRequestParameters = new HashMap<>(); + httpRequestParameters.put("key1", "value1"); + httpRequestParameters.put("key2", "value2"); + httpRequestParameters.put("key3", "value3"); + + DataSpec dataSpec = + new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + httpRequestParameters); + + DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse("www.new-uri.com")); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + + dataSpecCopy = dataSpec.subrange(2); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + + dataSpecCopy = dataSpec.subrange(2, 2); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + } +} From 9ca5b0fc61e441b1220fdef74dc9592e85f7c070 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 26 Aug 2019 11:03:39 +0100 Subject: [PATCH 358/807] Add back deprecated getWindow to ForwardingTimeline. Implementations of ForwardingTimeline may override any of the two variants of this method. We need to ensure that the customized override is always called. Add back the deprecated method and make it final to forward to the non-deprecated method in all cases for ForwardingTimelines. PiperOrigin-RevId: 265419830 --- .../android/exoplayer2/source/ForwardingTimeline.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index 38b373b26c..c36e62db86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -61,6 +61,12 @@ public abstract class ForwardingTimeline extends Timeline { return timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); } + @Override + public final Window getWindow( + int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { + return getWindow(windowIndex, window, defaultPositionProjectionUs); + } + @Override public int getPeriodCount() { return timeline.getPeriodCount(); From aa6ead3d080d6d3c8cc2971d70ebb5b3dc02ea83 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 27 Aug 2019 13:28:07 +0100 Subject: [PATCH 359/807] seenCacheError should be set for all errors PiperOrigin-RevId: 265662686 --- .../exoplayer2/upstream/cache/CacheDataSource.java | 9 ++++++--- .../android/exoplayer2/upstream/cache/CacheUtil.java | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) 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 6e20db7bf7..541c3b2d9d 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 @@ -285,7 +285,7 @@ public final class CacheDataSource implements DataSource { } openNextSource(false); return bytesRemaining; - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -327,6 +327,9 @@ public final class CacheDataSource implements DataSource { } handleBeforeThrow(e); throw e; + } catch (Throwable e) { + handleBeforeThrow(e); + throw e; } } @@ -353,7 +356,7 @@ public final class CacheDataSource implements DataSource { notifyBytesRead(); try { closeCurrentSource(); - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -520,7 +523,7 @@ public final class CacheDataSource implements DataSource { } } - private void handleBeforeThrow(IOException exception) { + private void handleBeforeThrow(Throwable exception) { if (isReadingFromCache() || exception instanceof CacheException) { seenCacheError = true; } 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 47470c5de7..6277ec686f 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 @@ -359,7 +359,7 @@ public final class CacheUtil { } } - /*package*/ static boolean isCausedByPositionOutOfRange(IOException e) { + /* package */ static boolean isCausedByPositionOutOfRange(IOException e) { Throwable cause = e; while (cause != null) { if (cause instanceof DataSourceException) { From 0a0ab8d5e7a3e7fd3894dcf4e736b5007d5b3174 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 27 Aug 2019 18:05:41 +0100 Subject: [PATCH 360/807] Avoid infinite recursion if cache file modified underneath cache This generalizes our "does file still exist" check to also check that the file is the expected length. If it's not, we don't trust it. This avoids infinite recursion in CacheDataSource if a cache file is truncated underneath the cache. Issue: #6165 PiperOrigin-RevId: 265707928 --- .../exoplayer2/upstream/cache/SimpleCache.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 81212b731f..e618fcad75 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 @@ -697,9 +697,9 @@ public final class SimpleCache implements Cache { } 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. + if (span.isCached && span.file.length() != span.length) { + // The file has been modified or deleted underneath us. It's likely that other files will + // have been modified too, so scan the whole in-memory representation. removeStaleSpans(); continue; } @@ -739,14 +739,14 @@ public final class SimpleCache implements Cache { } /** - * Scans all of the cached spans in the in-memory representation, removing any for which files no - * longer exist. + * Scans all of the cached spans in the in-memory representation, removing any for which the + * underlying file lengths no longer match. */ private void removeStaleSpans() { ArrayList spansToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : contentIndex.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { - if (!span.file.exists()) { + if (span.file.length() != span.length) { spansToBeRemoved.add(span); } } From 1b3cb639b3a897c7dc9707e61b8d80b4d6bc0014 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 29 Aug 2019 09:17:34 +0100 Subject: [PATCH 361/807] Signal when supplemental data is present for vp9 PiperOrigin-RevId: 266085854 --- .../android/exoplayer2/video/VideoDecoderOutputBuffer.java | 1 + 1 file changed, 1 insertion(+) 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 index 10ccb4eba2..3704a09da0 100644 --- 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 @@ -66,6 +66,7 @@ public abstract class VideoDecoderOutputBuffer extends OutputBuffer { this.timeUs = timeUs; this.mode = mode; if (supplementalData != null) { + addFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); int size = supplementalData.limit(); if (this.supplementalData == null || this.supplementalData.capacity() < size) { this.supplementalData = ByteBuffer.allocate(size); From 2451211294de865d307b6476b974f1429f13d121 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Aug 2019 21:34:13 +0100 Subject: [PATCH 362/807] Tweak TrackSelectionDialog PiperOrigin-RevId: 266216274 --- .../google/android/exoplayer2/demo/TrackSelectionDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d6fe6e2dc1..f92662f284 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 @@ -20,7 +20,6 @@ import android.content.DialogInterface; import android.content.res.Resources; import android.os.Bundle; import androidx.annotation.Nullable; -import com.google.android.material.tabs.TabLayout; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -39,6 +38,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.ui.TrackSelectionView; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import java.util.Collections; import java.util.List; From f5c1e8b5e31895c0497f1e89ebb64f59a99961f5 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Aug 2019 21:41:19 +0100 Subject: [PATCH 363/807] Add HttpDataSource.getResponseCode to provide the status code associated with the most recent HTTP response. PiperOrigin-RevId: 266218104 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ext/cronet/CronetDataSource.java | 7 +++++++ .../android/exoplayer2/ext/okhttp/OkHttpDataSource.java | 5 +++++ .../android/exoplayer2/upstream/DefaultHttpDataSource.java | 7 ++++++- .../google/android/exoplayer2/upstream/HttpDataSource.java | 6 ++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f7d5bea9c..c58841c2b3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,6 +46,8 @@ postroll ad ([#6314](https://github.com/google/ExoPlayer/issues/6314)). * Fix audio selection issue where languages are compared by bit rate ([#6335](https://github.com/google/ExoPlayer/issues/6335)). +* Add `HttpDataSource.getResponseCode` to provide the status code associated + with the most recent HTTP response. ### 2.10.4 ### 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 ed92523017..bff1672e62 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 @@ -392,6 +392,13 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { requestProperties.clear(); } + @Override + public int getResponseCode() { + return responseInfo == null || responseInfo.getHttpStatusCode() <= 0 + ? -1 + : responseInfo.getHttpStatusCode(); + } + @Override public Map> getResponseHeaders() { return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders(); 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 ec05c52f44..1a6a67b140 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 @@ -172,6 +172,11 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { return response == null ? null : Uri.parse(response.request().url().toString()); } + @Override + public int getResponseCode() { + return response == null ? -1 : response.code(); + } + @Override public Map> getResponseHeaders() { return response == null ? Collections.emptyMap() : response.headers().toMultimap(); 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 3ee1ef7564..e28c133a26 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 @@ -83,6 +83,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou @Nullable private HttpURLConnection connection; @Nullable private InputStream inputStream; private boolean opened; + private int responseCode; private long bytesToSkip; private long bytesToRead; @@ -234,6 +235,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou return connection == null ? null : Uri.parse(connection.getURL().toString()); } + @Override + public int getResponseCode() { + return connection == null || responseCode <= 0 ? -1 : responseCode; + } + @Override public Map> getResponseHeaders() { return connection == null ? Collections.emptyMap() : connection.getHeaderFields(); @@ -270,7 +276,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou dataSpec, HttpDataSourceException.TYPE_OPEN); } - int responseCode; String responseMessage; try { responseCode = connection.getResponseCode(); 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 17fb4ad7a1..778c116e01 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 @@ -357,6 +357,12 @@ public interface HttpDataSource extends DataSource { */ void clearAllRequestProperties(); + /** + * When the source is open, returns the HTTP response status code associated with the last {@link + * #open} call. Otherwise, returns a negative value. + */ + int getResponseCode(); + @Override Map> getResponseHeaders(); } From 48555550d7fcf6953f2382466818c74092b26355 Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 30 Aug 2019 10:17:02 +0100 Subject: [PATCH 364/807] Surface MediaCodecInfo methods added in Android Q - Surface information provided by methods isHardwareAccelerated, isSoftwareOnly and isVendor added in Android Q in MediaCodecInfo class. - Estimate this information based on the codec name for earlier API levels. Issue:#5839 PiperOrigin-RevId: 266334850 --- RELEASENOTES.md | 3 + .../exoplayer2/mediacodec/MediaCodecInfo.java | 45 +++++++++++ .../exoplayer2/mediacodec/MediaCodecUtil.java | 75 +++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c58841c2b3..41613f9cde 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### dev-v2 (not yet released) ### +* Surface information provided by methods `isHardwareAccelerated`, + `isSoftwareOnly` and `isVendor` added in Android Q in `MediaCodecInfo` class + ([#5839](https://github.com/google/ExoPlayer/issues/5839)). * Update `DefaultTrackSelector` to apply a viewport constraint for the default display by default. * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis 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 c700259b13..6dcbf896c9 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 @@ -93,6 +93,33 @@ public final class MediaCodecInfo { /** Whether this instance describes a passthrough codec. */ public final boolean passthrough; + /** + * Whether the codec is hardware accelerated. + * + *

        This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isHardwareAccelerated() + */ + public final boolean hardwareAccelerated; + + /** + * Whether the codec is software only. + * + *

        This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isSoftwareOnly() + */ + public final boolean softwareOnly; + + /** + * Whether the codec is from the vendor. + * + *

        This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isVendor() + */ + public final boolean vendor; + private final boolean isVideo; /** @@ -108,6 +135,9 @@ public final class MediaCodecInfo { /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); } @@ -121,6 +151,9 @@ public final class MediaCodecInfo { * 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 hardwareAccelerated Whether the {@link MediaCodec} is hardware accelerated. + * @param softwareOnly Whether the {@link MediaCodec} is software only. + * @param vendor Whether the {@link MediaCodec} is provided by the vendor. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. @@ -130,6 +163,9 @@ public final class MediaCodecInfo { String mimeType, String codecMimeType, @Nullable CodecCapabilities capabilities, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( @@ -138,6 +174,9 @@ public final class MediaCodecInfo { codecMimeType, capabilities, /* passthrough= */ false, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, forceSecure); } @@ -148,6 +187,9 @@ public final class MediaCodecInfo { @Nullable String codecMimeType, @Nullable CodecCapabilities capabilities, boolean passthrough, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); @@ -155,6 +197,9 @@ public final class MediaCodecInfo { this.codecMimeType = codecMimeType; this.capabilities = capabilities; this.passthrough = passthrough; + this.hardwareAccelerated = hardwareAccelerated; + this.softwareOnly = softwareOnly; + this.vendor = vendor; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); secure = forceSecure || (capabilities != null && isSecure(capabilities)); 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 9c42916cad..966e9fecc2 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 @@ -312,6 +312,9 @@ public final class MediaCodecUtil { if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { continue; } + boolean hardwareAccelerated = isHardwareAccelerated(codecInfo); + boolean softwareOnly = isSoftwareOnly(codecInfo); + boolean vendor = isVendor(codecInfo); boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(name); if ((secureDecodersExplicit && key.secure == secureSupported) || (!secureDecodersExplicit && !key.secure)) { @@ -321,6 +324,9 @@ public final class MediaCodecUtil { mimeType, codecMimeType, capabilities, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { @@ -330,6 +336,9 @@ public final class MediaCodecUtil { mimeType, codecMimeType, capabilities, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, /* forceSecure= */ true)); // It only makes sense to have one synthesized secure decoder, return immediately. @@ -532,6 +541,9 @@ public final class MediaCodecUtil { /* mimeType= */ MimeTypes.AUDIO_RAW, /* codecMimeType= */ MimeTypes.AUDIO_RAW, /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, /* forceDisableAdaptive= */ false, /* forceSecure= */ false)); } @@ -565,6 +577,69 @@ public final class MediaCodecUtil { } } + /** + * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+, + * or a best-effort approximation for lower levels. + */ + private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isHardwareAcceleratedV29(codecInfo); + } + // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true. + // However, we assume this to be true as an approximation. + return !isSoftwareOnly(codecInfo); + } + + @TargetApi(29) + private static boolean isHardwareAcceleratedV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isHardwareAccelerated(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isSoftwareOnlyV29(codecInfo); + } + String codecName = codecInfo.getName().toLowerCase(); + if (codecName.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs + return false; + } + return codecName.startsWith("omx.google.") + || codecName.startsWith("omx.ffmpeg.") + || (codecName.startsWith("omx.sec.") && codecName.contains(".sw.")) + || codecName.equals("omx.qcom.video.decoder.hevcswvdec") + || codecName.startsWith("c2.android.") + || codecName.startsWith("c2.google.") + || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")); + } + + @TargetApi(29) + private static boolean isSoftwareOnlyV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isSoftwareOnly(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isVendor()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isVendor(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isVendorV29(codecInfo); + } + String codecName = codecInfo.getName().toLowerCase(); + return !codecName.startsWith("omx.google.") + && !codecName.startsWith("c2.android.") + && !codecName.startsWith("c2.google."); + } + + @TargetApi(29) + private static boolean isVendorV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isVendor(); + } + /** * Returns whether the decoder is known to fail when adapting, despite advertising itself as an * adaptive decoder. From 9508146840c68215fe6b71eea7045de8efdf3866 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 30 Aug 2019 11:27:40 +0100 Subject: [PATCH 365/807] Introduce LoadErrorHandling policy in DefaultDrmSession This is a no-op interim change to introduce key request error handling customization. Following changes will allow users to inject their own LoadErrorHandlingPolicy implementations. Issue:#6334 PiperOrigin-RevId: 266344399 --- .../exoplayer2/drm/DefaultDrmSession.java | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) 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 c83214c8d5..6a65209841 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 @@ -28,10 +28,13 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -47,6 +50,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @TargetApi(18) public class DefaultDrmSession implements DrmSession { + /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */ + public static final class UnexpectedDrmSessionException extends IOException { + + public UnexpectedDrmSessionException(Throwable cause) { + super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); + } + } + /** Manages provisioning requests. */ public interface ProvisioningManager { @@ -97,7 +108,7 @@ public class DefaultDrmSession implements DrmSession optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; - private final int initialDrmRequestRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; @@ -164,8 +175,10 @@ public class DefaultDrmSession implements DrmSession implements DrmSession implements DrmSession initialDrmRequestRetryCount) { + if (errorCount > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) { return false; } Message retryMsg = Message.obtain(originalMsg); retryMsg.arg2 = errorCount; - sendMessageDelayed(retryMsg, getRetryDelayMillis(errorCount)); - return true; - } - private long getRetryDelayMillis(int errorCount) { - return Math.min((errorCount - 1) * 1000, 5000); + IOException ioException = + e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e); + // TODO: Add loadDurationMs calculation before allowing user-provided load error handling + // policies. + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + C.DATA_TYPE_DRM, /* loadDurationMs= */ C.TIME_UNSET, ioException, errorCount); + sendMessageDelayed(retryMsg, retryDelayMs); + return true; } } } From 967abdf0f5f749cb80731fad412032ab33c3c0bb Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 30 Aug 2019 12:19:09 +0100 Subject: [PATCH 366/807] Use DataSpec request params in DefaultHttpDataSource DefaultHttpDataSource.open() also includes the request parameters that are inside the DataSpec. PiperOrigin-RevId: 266350573 --- .../upstream/DefaultHttpDataSource.java | 44 ++++-- .../upstream/DefaultHttpDataSourceTest.java | 142 ++++++++++++++++++ 2 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java 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 e28c133a26..f38b0709f9 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.icy.IcyHeaders; @@ -36,6 +37,7 @@ import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URL; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -53,9 +55,7 @@ import java.util.zip.GZIPInputStream; */ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { - /** - * The default connection timeout, in milliseconds. - */ + /** The default connection timeout, in milliseconds. */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; /** * The default read timeout, in milliseconds. @@ -263,6 +263,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou requestProperties.clear(); } + /** + * Opens the source to read the specified data. + * + *

        Note: HTTP request headers will be set using parameters passed via (in order of decreasing + * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to + * construct the instance. + */ @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; @@ -374,6 +381,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } + /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */ + @VisibleForTesting + /* package */ + HttpURLConnection openConnection(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + /** * Returns the current connection, or null if the source is not currently opened. * @@ -438,7 +452,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou length, allowGzip, allowIcyMetadata, - /* followRedirects= */ true); + /* followRedirects= */ true, + dataSpec.httpRequestHeaders); } // We need to handle redirects ourselves to allow cross-protocol redirects. @@ -453,7 +468,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou length, allowGzip, allowIcyMetadata, - /* followRedirects= */ false); + /* followRedirects= */ false, + dataSpec.httpRequestHeaders); int responseCode = connection.getResponseCode(); String location = connection.getHeaderField("Location"); if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) @@ -495,6 +511,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param allowGzip Whether to allow the use of gzip. * @param allowIcyMetadata Whether to allow ICY metadata. * @param followRedirects Whether to follow redirects. + * @param requestParameters parameters (HTTP headers) to include in request. */ private HttpURLConnection makeConnection( URL url, @@ -504,19 +521,24 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou long length, boolean allowGzip, boolean allowIcyMetadata, - boolean followRedirects) + boolean followRedirects, + Map requestParameters) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + HttpURLConnection connection = openConnection(url); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); + + Map requestHeaders = new HashMap<>(); if (defaultRequestProperties != null) { - for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { - connection.setRequestProperty(property.getKey(), property.getValue()); - } + requestHeaders.putAll(defaultRequestProperties.getSnapshot()); } - for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + requestHeaders.putAll(requestProperties.getSnapshot()); + requestHeaders.putAll(requestParameters); + + for (Map.Entry property : requestHeaders.entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } + if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java new file mode 100644 index 0000000000..7c2b63c941 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java @@ -0,0 +1,142 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +/** Unit tests for {@link DefaultHttpDataSource}. */ +@RunWith(AndroidJUnit4.class) +public class DefaultHttpDataSourceTest { + + @Test + public void open_withSpecifiedRequestParameters_usesCorrectParameters() throws IOException { + + /* + * This test will set HTTP request parameters in the HttpDataSourceFactory (default), + * in the DefaultHttpDataSource instance and in the DataSpec instance according to the table + * below. Values wrapped in '*' are the ones that should be set in the connection request. + * + * +-----------------------+---+-----+-----+-----+-----+-----+ + * | | Header Key | + * +-----------------------+---+-----+-----+-----+-----+-----+ + * | Location | 0 | 1 | 2 | 3 | 4 | 5 | + * +-----------------------+---+-----+-----+-----+-----+-----+ + * | Default |*Y*| Y | Y | | | | + * | DefaultHttpDataSource | | *Y* | Y | Y | *Y* | | + * | DataSpec | | | *Y* | *Y* | | *Y* | + * +-----------------------+---+-----+-----+-----+-----+-----+ + */ + + String defaultParameter = "Default"; + String dataSourceInstanceParameter = "DefaultHttpDataSource"; + String dataSpecParameter = "Dataspec"; + + HttpDataSource.RequestProperties defaultParameters = new HttpDataSource.RequestProperties(); + defaultParameters.set("0", defaultParameter); + defaultParameters.set("1", defaultParameter); + defaultParameters.set("2", defaultParameter); + + DefaultHttpDataSource defaultHttpDataSource = + Mockito.spy( + new DefaultHttpDataSource( + /* userAgent= */ "testAgent", + /* connectTimeoutMillis= */ 1000, + /* readTimeoutMillis= */ 1000, + /* allowCrossProtocolRedirects= */ false, + defaultParameters)); + + Map sentRequestProperties = new HashMap<>(); + HttpURLConnection mockHttpUrlConnection = makeMockHttpUrlConnection(sentRequestProperties); + Mockito.doReturn(mockHttpUrlConnection) + .when(defaultHttpDataSource) + .openConnection(ArgumentMatchers.any()); + + defaultHttpDataSource.setRequestProperty("1", dataSourceInstanceParameter); + defaultHttpDataSource.setRequestProperty("2", dataSourceInstanceParameter); + defaultHttpDataSource.setRequestProperty("3", dataSourceInstanceParameter); + defaultHttpDataSource.setRequestProperty("4", dataSourceInstanceParameter); + + Map dataSpecRequestProperties = new HashMap<>(); + dataSpecRequestProperties.put("2", dataSpecParameter); + dataSpecRequestProperties.put("3", dataSpecParameter); + dataSpecRequestProperties.put("5", dataSpecParameter); + + DataSpec dataSpec = + new DataSpec( + /* uri= */ Uri.parse("http://www.google.com"), + /* httpMethod= */ 1, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + dataSpecRequestProperties); + + defaultHttpDataSource.open(dataSpec); + + assertThat(sentRequestProperties.get("0")).isEqualTo(defaultParameter); + assertThat(sentRequestProperties.get("1")).isEqualTo(dataSourceInstanceParameter); + assertThat(sentRequestProperties.get("2")).isEqualTo(dataSpecParameter); + assertThat(sentRequestProperties.get("3")).isEqualTo(dataSpecParameter); + assertThat(sentRequestProperties.get("4")).isEqualTo(dataSourceInstanceParameter); + assertThat(sentRequestProperties.get("5")).isEqualTo(dataSpecParameter); + } + + /** + * Creates a mock {@link HttpURLConnection} that stores all request parameters inside {@code + * requestProperties}. + */ + private static HttpURLConnection makeMockHttpUrlConnection(Map requestProperties) + throws IOException { + HttpURLConnection mockHttpUrlConnection = Mockito.mock(HttpURLConnection.class); + Mockito.when(mockHttpUrlConnection.usingProxy()).thenReturn(false); + + Mockito.when(mockHttpUrlConnection.getInputStream()) + .thenReturn(new ByteArrayInputStream(new byte[128])); + + Mockito.when(mockHttpUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + + Mockito.when(mockHttpUrlConnection.getResponseCode()).thenReturn(200); + Mockito.when(mockHttpUrlConnection.getResponseMessage()).thenReturn("OK"); + + Mockito.doAnswer( + (invocation) -> { + String key = invocation.getArgument(0); + String value = invocation.getArgument(1); + requestProperties.put(key, value); + return null; + }) + .when(mockHttpUrlConnection) + .setRequestProperty(ArgumentMatchers.anyString(), ArgumentMatchers.anyString()); + + return mockHttpUrlConnection; + } +} From d720d2c3f6d8cfb94c50a3b2ed1bef9e00cda274 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Aug 2019 17:35:29 +0100 Subject: [PATCH 367/807] Simplify androidTest manifests & fix links to use https PiperOrigin-RevId: 266396506 --- extensions/flac/src/androidTest/AndroidManifest.xml | 2 +- extensions/opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- library/core/src/androidTest/AndroidManifest.xml | 2 +- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 39b92aa217..6736ab4b16 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml index 7f775f4d32..031960636d 100644 --- a/extensions/opus/src/androidTest/AndroidManifest.xml +++ b/extensions/opus/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml index 6ca2e7164a..4d0832d198 100644 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index e6e874a27a..831ad47831 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index be71884846..b6c6064227 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -23,7 +23,7 @@ - From a02237de20a88db91258d150a29939483ccefdf9 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 1 Sep 2019 21:52:02 +0100 Subject: [PATCH 368/807] Fix imports PiperOrigin-RevId: 266676413 --- demos/cast/build.gradle | 4 ++-- .../exoplayer2/castdemo/MainActivity.java | 18 +++++++++--------- .../exoplayer2/gvrdemo/PlayerActivity.java | 2 +- demos/main/build.gradle | 2 -- .../exoplayer2/demo/DownloadTracker.java | 2 +- .../exoplayer2/demo/PlayerActivity.java | 6 +++--- .../exoplayer2/demo/SampleChooserActivity.java | 4 ++-- .../exoplayer2/demo/TrackSelectionDialog.java | 12 ++++++------ .../exoplayer2/ext/cast/CastTimeline.java | 2 +- .../ext/cronet/CronetDataSource.java | 2 +- .../exoplayer2/ext/gvr/GvrPlayerActivity.java | 6 +++--- extensions/ima/build.gradle | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 2 +- .../ext/leanback/LeanbackPlayerAdapter.java | 2 +- .../mediasession/MediaSessionConnector.java | 6 +++--- .../mediasession/RepeatModeActionProvider.java | 2 +- .../ext/mediasession/TimelineQueueEditor.java | 4 ++-- .../mediasession/TimelineQueueNavigator.java | 2 +- .../ext/vp9/LibvpxVideoRenderer.java | 2 +- .../android/exoplayer2/ext/vp9/VpxDecoder.java | 2 +- .../ext/vp9/VpxVideoSurfaceView.java | 2 +- .../java/com/google/android/exoplayer2/C.java | 2 +- .../android/exoplayer2/ExoPlayerImpl.java | 2 +- .../exoplayer2/ExoPlayerImplInternal.java | 2 +- .../android/exoplayer2/MediaPeriodQueue.java | 2 +- .../com/google/android/exoplayer2/Player.java | 4 ++-- .../android/exoplayer2/SimpleExoPlayer.java | 4 ++-- .../google/android/exoplayer2/Timeline.java | 2 +- .../analytics/AnalyticsCollector.java | 2 +- .../analytics/AnalyticsListener.java | 2 +- .../DefaultPlaybackSessionManager.java | 2 +- .../exoplayer2/analytics/PlaybackStats.java | 2 +- .../analytics/PlaybackStatsListener.java | 2 +- .../exoplayer2/drm/DefaultDrmSession.java | 2 +- .../drm/DefaultDrmSessionManager.java | 2 +- .../android/exoplayer2/drm/DrmInitData.java | 2 +- .../exoplayer2/drm/FrameworkMediaDrm.java | 2 +- .../exoplayer2/drm/HttpMediaDrmCallback.java | 2 +- .../exoplayer2/drm/OfflineLicenseHelper.java | 2 +- .../android/exoplayer2/drm/WidevineUtil.java | 2 +- .../extractor/mkv/MatroskaExtractor.java | 4 ++-- .../exoplayer2/extractor/mp4/AtomParsers.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 4 ++-- .../ts/DefaultTsPayloadReaderFactory.java | 2 +- .../exoplayer2/extractor/ts/LatmReader.java | 2 +- .../exoplayer2/extractor/ts/TsExtractor.java | 2 +- .../extractor/ts/TsPayloadReader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 2 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 4 ++-- .../exoplayer2/offline/DownloadHelper.java | 2 +- .../exoplayer2/offline/SegmentDownloader.java | 2 +- .../exoplayer2/source/ClippingMediaSource.java | 1 - .../exoplayer2/source/MaskingMediaSource.java | 2 +- .../exoplayer2/source/ads/AdsLoader.java | 2 +- .../source/chunk/ChunkExtractorWrapper.java | 2 +- .../exoplayer2/text/CaptionStyleCompat.java | 4 ++-- .../google/android/exoplayer2/text/Cue.java | 2 +- .../android/exoplayer2/text/cea/Cea708Cue.java | 2 +- .../exoplayer2/text/ssa/SsaDecoder.java | 2 +- .../exoplayer2/text/subrip/SubripDecoder.java | 2 +- .../android/exoplayer2/text/ttml/TtmlNode.java | 2 +- .../exoplayer2/text/ttml/TtmlStyle.java | 2 +- .../exoplayer2/text/webvtt/CssParser.java | 2 +- .../exoplayer2/text/webvtt/WebvttCssStyle.java | 2 +- .../text/webvtt/WebvttCueParser.java | 2 +- .../BufferSizeAdaptationBuilder.java | 2 +- .../trackselection/DefaultTrackSelector.java | 2 +- .../trackselection/MappingTrackSelector.java | 2 +- .../TrackSelectionParameters.java | 2 +- .../upstream/DataSchemeDataSource.java | 2 +- .../upstream/DefaultBandwidthMeter.java | 2 +- .../upstream/DefaultHttpDataSource.java | 2 +- .../exoplayer2/upstream/HttpDataSource.java | 2 +- .../upstream/RawResourceDataSource.java | 2 +- .../exoplayer2/upstream/cache/CacheUtil.java | 2 +- .../upstream/cache/CachedContentIndex.java | 4 ++-- .../android/exoplayer2/util/Assertions.java | 2 +- .../exoplayer2/util/CodecSpecificDataUtil.java | 2 +- .../android/exoplayer2/util/EventLogger.java | 2 +- .../google/android/exoplayer2/util/Log.java | 2 +- .../android/exoplayer2/util/MimeTypes.java | 2 +- .../android/exoplayer2/util/UriUtil.java | 2 +- .../google/android/exoplayer2/util/Util.java | 2 +- .../android/exoplayer2/video/DummySurface.java | 2 +- .../video/MediaCodecVideoRenderer.java | 4 ++-- .../video/SimpleDecoderVideoRenderer.java | 2 +- .../video/VideoFrameReleaseTimeHelper.java | 2 +- .../video/VideoRendererEventListener.java | 2 +- .../android/exoplayer2/ExoPlayerTest.java | 2 +- .../analytics/AnalyticsCollectorTest.java | 2 +- .../upstream/cache/CachedContentIndexTest.java | 2 +- .../source/dash/DashMediaPeriod.java | 4 ++-- .../source/dash/DashMediaSource.java | 2 +- .../dash/manifest/DashManifestParser.java | 2 +- .../source/hls/DefaultHlsExtractorFactory.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- .../source/hls/HlsTrackMetadataEntry.java | 2 +- .../exoplayer2/source/hls/WebvttExtractor.java | 2 +- .../source/hls/playlist/HlsPlaylistParser.java | 2 +- .../manifest/SsManifestParser.java | 2 +- .../exoplayer2/ui/AspectRatioFrameLayout.java | 4 ++-- .../android/exoplayer2/ui/DefaultTimeBar.java | 4 ++-- .../exoplayer2/ui/PlayerControlView.java | 2 +- .../ui/PlayerNotificationManager.java | 2 +- .../android/exoplayer2/ui/PlayerView.java | 6 +++--- .../exoplayer2/ui/SimpleExoPlayerView.java | 2 +- .../android/exoplayer2/ui/SubtitleView.java | 2 +- .../google/android/exoplayer2/ui/TimeBar.java | 2 +- .../ui/TrackSelectionDialogBuilder.java | 2 +- .../exoplayer2/ui/TrackSelectionView.java | 4 ++-- .../ui/spherical/CanvasRenderer.java | 2 +- .../exoplayer2/ui/spherical/GlViewGroup.java | 4 ++-- .../ui/spherical/OrientationListener.java | 2 +- .../ui/spherical/SphericalSurfaceView.java | 8 ++++---- .../exoplayer2/ui/spherical/TouchTracker.java | 4 ++-- .../android/exoplayer2/testutil/Action.java | 2 +- .../exoplayer2/testutil/ActionSchedule.java | 2 +- .../testutil/MediaSourceTestRunner.java | 2 +- 119 files changed, 159 insertions(+), 162 deletions(-) diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 85e60f2796..60b9cdbc4e 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -56,10 +56,10 @@ dependencies { implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') - implementation 'com.google.android.material:material:1.0.0' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'com.google.android.material:material:1.0.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index d0e40990be..0c5b5037f5 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -17,15 +17,6 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.graphics.ColorUtils; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.ViewHolder; -import androidx.recyclerview.widget.ItemTouchHelper; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -36,6 +27,15 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; 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 index 15cc9b6469..7023693d03 100644 --- 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 @@ -18,8 +18,8 @@ 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 androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.DefaultRenderersFactory; diff --git a/demos/main/build.gradle b/demos/main/build.gradle index f58389d9d4..be08ca9ea2 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -63,8 +63,6 @@ android { dependencies { 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' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') 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 839ed304bd..440a25a289 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 @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.demo; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentManager; -import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.Download; 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 d3c32ac957..347f49e27c 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 @@ -19,9 +19,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.util.Pair; import android.view.KeyEvent; import android.view.View; @@ -30,6 +27,9 @@ import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; 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 09fa62e51a..3920cd3a80 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 @@ -21,8 +21,6 @@ import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.util.JsonReader; import android.view.Menu; import android.view.MenuInflater; @@ -36,6 +34,8 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.demo.Sample.DrmInfo; 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 f92662f284..9e8009388e 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 @@ -19,17 +19,17 @@ import android.app.Dialog; import android.content.DialogInterface; import android.content.res.Resources; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.appcompat.app.AppCompatDialog; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroupArray; 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 58dbec611a..a0741b23cc 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 @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.ext.cast; -import androidx.annotation.Nullable; import android.util.SparseArray; import android.util.SparseIntArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import java.util.Arrays; 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 bff1672e62..241b879b9a 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 @@ -18,8 +18,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; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.metadata.icy.IcyHeaders; 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 e22c97859a..06b9fac487 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 @@ -23,13 +23,13 @@ import android.opengl.Matrix; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.view.ContextThemeWrapper; +import android.view.MotionEvent; +import android.view.Surface; import androidx.annotation.BinderThread; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import androidx.annotation.UiThread; -import android.view.ContextThemeWrapper; -import android.view.MotionEvent; -import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ui.PlayerControlView; diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 340e9832be..e330b13e47 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -35,7 +35,7 @@ dependencies { 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' + implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } 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 3b2bcd8880..cf0ea79da6 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 @@ -19,11 +19,11 @@ import android.content.Context; import android.net.Uri; import android.os.Looper; import android.os.SystemClock; +import android.view.View; +import android.view.ViewGroup; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.view.View; -import android.view.ViewGroup; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; 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 98aee17d73..2995df4ab4 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 @@ -22,10 +22,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Uri; -import androidx.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.ads.interactivemedia.v3.api.Ad; 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 370e5515e8..7eb8dac49f 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 @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.ext.leanback; import android.content.Context; import android.os.Handler; -import androidx.annotation.Nullable; import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; +import androidx.annotation.Nullable; import androidx.leanback.R; import androidx.leanback.media.PlaybackGlueHost; import androidx.leanback.media.PlayerAdapter; 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 cb1788f2fc..3108476964 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 @@ -23,9 +23,6 @@ import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; import android.os.SystemClock; -import androidx.annotation.LongDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; @@ -33,6 +30,9 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Pair; +import androidx.annotation.LongDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; 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 5c969dd44d..87b9447f7c 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,8 +17,8 @@ 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 androidx.annotation.Nullable; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.util.RepeatModeUtil; 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 d72f6ffddc..d5fed2958b 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 @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.ext.mediasession; import android.os.Bundle; import android.os.ResultReceiver; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.Player; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index b89a6f4eab..fc4cc11b58 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.ext.mediasession; import android.os.Bundle; import android.os.ResultReceiver; -import androidx.annotation.Nullable; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.Player; 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 dd4077964b..13f020031c 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 @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.ext.vp9; import static java.lang.Runtime.getRuntime; import android.os.Handler; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; 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 5630058712..f6b1ddccea 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,8 +15,8 @@ */ package com.google.android.exoplayer2.ext.vp9; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.SimpleDecoder; 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 4e983cccc7..9dd2432622 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,8 +17,8 @@ package com.google.android.exoplayer2.ext.vp9; import android.content.Context; import android.opengl.GLSurfaceView; -import androidx.annotation.Nullable; import android.util.AttributeSet; +import androidx.annotation.Nullable; /** * A GLSurfaceView extension that scales itself to the given aspect ratio. 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 d073eb4ee0..38553697d0 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 @@ -21,8 +21,8 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaFormat; -import androidx.annotation.IntDef; import android.view.Surface; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.util.Util; 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 38d66e5cbc..15a0e3a7bd 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 @@ -19,8 +19,8 @@ import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import android.os.Message; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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 a6e8c679a8..8beb7d781e 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 @@ -21,9 +21,9 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.MediaPeriod; 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 e515877d78..22c4119021 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; 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 eed59876f9..f4d3dfdeda 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 @@ -16,12 +16,12 @@ package com.google.android.exoplayer2; import android.os.Looper; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C.VideoScalingMode; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioListener; 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 b5956c9dae..795eca5ea0 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 @@ -23,12 +23,12 @@ import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; import android.os.Looper; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; 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 57d3d8bf1d..0018a15157 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; 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 1ccd4ffc84..43154a4b3f 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.analytics; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; 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 be62ad99d2..656548df47 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.analytics; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; 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 183a74544d..44f8c10afe 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.analytics; -import androidx.annotation.Nullable; import android.util.Base64; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; 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 index ed127bc550..bd8fb213ed 100644 --- 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.analytics; import android.os.SystemClock; -import androidx.annotation.IntDef; import android.util.Pair; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; 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 index 6444b4747f..8b9f1d1ced 100644 --- 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.analytics; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; 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 6a65209841..a667c8374a 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 @@ -22,8 +22,8 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; 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 34fd223c64..042a30e440 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 @@ -20,9 +20,9 @@ import android.annotation.TargetApi; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; 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 7cc2231e0f..2f0246ba64 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 @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.drm; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; 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 e77504c91c..15a4b3da4a 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 @@ -23,8 +23,8 @@ import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; 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 23b2300dfa..19c32daf61 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 @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; 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 05dab7e42d..03c199bda0 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 @@ -19,8 +19,8 @@ import android.media.MediaDrm; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java index 9fed3b38e8..004f873a33 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.drm; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.Map; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index e4f42fcf91..56ba01b967 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -15,11 +15,11 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import android.util.Pair; +import android.util.SparseArray; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.util.Pair; -import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; 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 b3c26246e6..2800c6069e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.mp4; import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; 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 5eaa5d5d31..f6e0fb8bc9 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 @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer2.extractor.mp4; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import android.util.Pair; import android.util.SparseArray; +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.ParserException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 15c37430bc..24d17f4956 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.annotation.IntDef; import android.util.SparseArray; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.text.cea.Cea708InitializationData; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 39e74ae6a2..4ad9adfa2a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; 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 d198e816d5..04dd7df385 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 @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR; -import androidx.annotation.IntDef; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 536a31c9fc..af27235257 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.annotation.IntDef; import android.util.SparseArray; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; 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 6dcbf896c9..1726648793 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 @@ -22,8 +22,8 @@ import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; 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 966e9fecc2..eb97d546e5 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 @@ -20,11 +20,11 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; -import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseIntArray; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; 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 54360f8f6b..de6b3f447e 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 @@ -20,8 +20,8 @@ import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; -import androidx.annotation.Nullable; import android.util.SparseIntArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.RendererCapabilities; 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 5326220452..808df6d36f 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 @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; +import android.util.Pair; import androidx.annotation.NonNull; 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.DataSpec; 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 703f8bafc0..4780c075d5 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,7 +15,6 @@ */ package com.google.android.exoplayer2.source; - import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; 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 index 35800f56da..671bc9eb74 100644 --- 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.source; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 2b90fac6ab..11947218a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.source.ads; -import androidx.annotation.Nullable; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index fc07a318b1..c4c8647a55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.source.chunk; -import androidx.annotation.Nullable; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DummyTrackOutput; 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 a7ab93a6dd..51aec3638f 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 @@ -18,10 +18,10 @@ package com.google.android.exoplayer2.text; 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 androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; 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 39359a9367..3dea9417c5 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 @@ -17,9 +17,9 @@ package com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; +import android.text.Layout.Alignment; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.Layout.Alignment; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java index a0201e19e6..fc1f0e2bdc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.text.cea; -import androidx.annotation.NonNull; import android.text.Layout.Alignment; +import androidx.annotation.NonNull; import com.google.android.exoplayer2.text.Cue; /** 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 e305259cbc..8da37e7f8f 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,8 +15,8 @@ */ package com.google.android.exoplayer2.text.ssa; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; 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 eb2b704bee..8d1b743a6d 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 @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer2.text.subrip; -import androidx.annotation.Nullable; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; +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; 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 3b4d061aaa..3365749e1a 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 @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.text.ttml; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import androidx.annotation.Nullable; import android.text.SpannableStringBuilder; import android.util.Base64; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index 9fdcc48c12..e90b099173 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.text.ttml; import android.graphics.Typeface; -import androidx.annotation.IntDef; import android.text.Layout; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; 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 c5d0526eb5..9a5ac40a05 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,8 +15,8 @@ */ package com.google.android.exoplayer2.text.webvtt; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index ded7ef73ff..2bd1af01e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; -import androidx.annotation.IntDef; import android.text.Layout; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 2361c9729f..bb639bfb7d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; -import androidx.annotation.NonNull; import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -31,6 +30,7 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.NonNull; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java index 9826c5b137..b850a08aeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.trackselection; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.Format; 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 21dff0b4b2..4e8da959ba 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,11 +19,11 @@ import android.content.Context; import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; 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 5587af9cbf..425da6c1c4 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 @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.trackselection; +import android.util.Pair; import androidx.annotation.IntDef; 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.Renderer; 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 c406f262d1..1582fabc88 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 @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.trackselection; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; 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 94a6e21c86..55c580ead2 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 @@ -18,8 +18,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; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.Util; 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 67dd9a7a55..da120414b4 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 @@ -22,8 +22,8 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.os.Handler; import android.os.Looper; -import androidx.annotation.Nullable; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; 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 f38b0709f9..0cf54a153c 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 @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; 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 778c116e01..4d3aca570e 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 @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.upstream; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; import java.io.IOException; 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 ff032a4ed0..fbfd698610 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 @@ -21,8 +21,8 @@ import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; 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 6277ec686f..bbec189b4d 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 3749ecfc9a..22086f8ac8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -21,10 +21,10 @@ import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index b4ccc5bcc4..9a4891d329 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.util; import android.os.Looper; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 16a891dbc6..f4f143f3b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.util; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import java.util.ArrayList; 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 cde9a351d9..c37e98776e 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.util; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java index 1eb0977847..a29460b84c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.util; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.TextUtils; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; 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 61457c308d..de30cfd214 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.util; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.ArrayList; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java index 60f4fa17dd..90be8660c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.util; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; /** * Utility methods for manipulating URIs. 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 35a36c3cd5..5c50849010 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 @@ -40,11 +40,11 @@ import android.os.Handler; import android.os.Looper; import android.os.Parcel; import android.security.NetworkSecurityPolicy; -import androidx.annotation.Nullable; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; 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 920d569fd3..d1f874b428 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 @@ -29,8 +29,8 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EGLSurfaceTexture; import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; 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 0310800876..10827f8aa7 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 @@ -26,11 +26,11 @@ import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; +import android.util.Pair; +import android.view.Surface; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Pair; -import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index ad59d11c4a..83f528e322 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -18,10 +18,10 @@ package com.google.android.exoplayer2.video; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.view.Surface; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.view.Surface; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index caa79d7c18..bf31ce2abb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -21,11 +21,11 @@ import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; -import androidx.annotation.Nullable; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; import android.view.Display; import android.view.WindowManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 2f76a2c23d..70f30d3280 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -17,9 +17,9 @@ package com.google.android.exoplayer2.video; import android.os.Handler; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.view.Surface; import android.view.TextureView; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderCounters; 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 f924dfb34c..013acc5ee2 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 @@ -21,8 +21,8 @@ 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.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player.DiscontinuityReason; 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 875f8b5d7b..ae87201c16 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 @@ -19,8 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import android.os.Handler; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index cee5703ff8..e28277d945 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -19,8 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; -import androidx.annotation.Nullable; import android.util.SparseArray; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; 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 21fd43da21..d62474cb1b 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 @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer2.source.dash; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import android.util.Pair; import android.util.SparseIntArray; +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.SeekParameters; 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 1b163c02e2..c9a039a4bd 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 @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; 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 9f6cce672e..42436d9593 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,11 +16,11 @@ 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; import android.util.Xml; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; 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 6dd4ade590..d5b9ca478e 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; 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 ad2a3ad265..7d47cc43c6 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; 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 2ba3b45ca0..f26a9b8e9a 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 @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.source.hls; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import java.util.ArrayList; import java.util.Collections; 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 a89e907a37..8fa79befca 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,8 +15,8 @@ */ package com.google.android.exoplayer2.source.hls; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 030520f8cb..21c29c6d87 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; 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 c08a867f37..d395e95fd9 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,10 +16,10 @@ 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; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; 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 268219b6d5..a39a0bab4f 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 @@ -17,10 +17,10 @@ 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 androidx.annotation.IntDef; +import androidx.annotation.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; 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 69a2cf96be..d0e621547e 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 @@ -25,8 +25,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; -import androidx.annotation.ColorInt; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.KeyEvent; @@ -36,6 +34,8 @@ import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; 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 3a194e091a..0765105314 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 @@ -22,7 +22,6 @@ import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Looper; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -32,6 +31,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackPreparer; 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 c69cb21704..8ad00e1dda 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 @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.os.Handler; import android.os.Looper; +import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -32,7 +33,6 @@ import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.media.app.NotificationCompat.MediaStyle; -import android.support.v4.media.session.MediaSessionCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; 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 0d66922cab..67701cbb93 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 @@ -27,9 +27,6 @@ import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Looper; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -41,6 +38,9 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 955fab14c9..e55666e178 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.ui; import android.content.Context; +import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.AttributeSet; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; 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 0bdc1acc88..5fa2e4816b 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 @@ -19,11 +19,11 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.accessibility.CaptioningManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java index d62f2494d5..9e3f2e42ff 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.ui; -import androidx.annotation.Nullable; import android.view.View; +import androidx.annotation.Nullable; /** * Interface for time bar views that can display a playback position, buffered position, duration diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java index 0df3fff5fe..f8a016bc8b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.ui; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; -import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; 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 02ed0a534e..79990e53a6 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 @@ -17,8 +17,6 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.TypedArray; -import androidx.annotation.AttrRes; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.util.Pair; import android.util.SparseArray; @@ -26,6 +24,8 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.CheckedTextView; import android.widget.LinearLayout; +import androidx.annotation.AttrRes; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; 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 6ef9d4907d..ed9be4ea7e 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 @@ -24,8 +24,8 @@ import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.opengl.GLES11Ext; import android.opengl.GLES20; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.GlUtil; import java.nio.FloatBuffer; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlViewGroup.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlViewGroup.java index 9ff6fcaf1f..37ac8e98d0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlViewGroup.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlViewGroup.java @@ -22,12 +22,12 @@ import android.graphics.Color; import android.graphics.PointF; import android.graphics.PorterDuff; import android.os.SystemClock; -import androidx.annotation.AnyThread; -import androidx.annotation.UiThread; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.AnyThread; +import androidx.annotation.UiThread; import com.google.android.exoplayer2.util.Assertions; /** This View uses standard Android APIs to render its child Views to a texture. */ diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/OrientationListener.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/OrientationListener.java index 80de418199..7276953cf5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/OrientationListener.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/OrientationListener.java @@ -20,9 +20,9 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.opengl.Matrix; -import androidx.annotation.BinderThread; import android.view.Display; import android.view.Surface; +import androidx.annotation.BinderThread; import com.google.android.exoplayer2.video.spherical.FrameRotationQueue; /** 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 67bc992558..419f31436a 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 @@ -25,15 +25,15 @@ import android.opengl.GLSurfaceView; import android.opengl.Matrix; import android.os.Handler; import android.os.Looper; +import android.util.AttributeSet; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; import androidx.annotation.AnyThread; import androidx.annotation.BinderThread; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; -import android.util.AttributeSet; -import android.view.Display; -import android.view.Surface; -import android.view.WindowManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.util.Assertions; 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 5f3a5275c1..66a0f20091 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 @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.ui.spherical; import android.content.Context; import android.graphics.PointF; -import androidx.annotation.BinderThread; -import androidx.annotation.Nullable; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.BinderThread; +import androidx.annotation.Nullable; /** * Basic touch input system. 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 5d07f986d2..c4116c3696 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; 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 735156e64c..1b0a00624e 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 @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.testutil; import android.os.Looper; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 211e85d30c..29d1b0bd75 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -22,8 +22,8 @@ import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; From d9529479fac6e91a002b215d71027159ffa25cda Mon Sep 17 00:00:00 2001 From: wingyippp Date: Wed, 4 Sep 2019 00:32:16 +0800 Subject: [PATCH 369/807] get 2 SeekPoints from the getSeekPosition() in flac_parser --- .../exoplayer2/ext/flac/FlacDecoderJni.java | 4 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 11 ++++++--- extensions/flac/src/main/jni/flac_jni.cc | 8 +++++-- extensions/flac/src/main/jni/flac_parser.cc | 23 +++++++++++++++---- .../flac/src/main/jni/include/flac_parser.h | 2 +- 5 files changed, 36 insertions(+), 12 deletions(-) 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 f454e28c68..6df783da78 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 @@ -223,7 +223,7 @@ import java.nio.ByteBuffer; * @return The corresponding position (byte offset) in the flac stream or -1 if the stream doesn't * have a seek table. */ - public long getSeekPosition(long timeUs) { + public long[] getSeekPosition(long timeUs) { return flacGetSeekPosition(nativeDecoderContext, timeUs); } @@ -283,7 +283,7 @@ import java.nio.ByteBuffer; private native long flacGetNextFrameFirstSampleIndex(long context); - private native long flacGetSeekPosition(long context, long timeUs); + private native long[] flacGetSeekPosition(long context, long timeUs); private native String flacGetStateString(long context); 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 cd91b06288..03416abd06 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 @@ -276,7 +276,8 @@ public final class FlacExtractor implements Extractor { FlacStreamMetadata streamMetadata, long streamLength, ExtractorOutput output) { - boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + long[] result = decoderJni.getSeekPosition(/* timeUs= */ 0); + boolean hasSeekTable = result.length == 4 && result[1] != -1 && result[3] != -1; FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (hasSeekTable) { @@ -341,8 +342,12 @@ public final class FlacExtractor implements Extractor { @Override public SeekPoints getSeekPoints(long timeUs) { - // TODO: Access the seek table via JNI to return two seek points when appropriate. - return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs))); + long[] result = decoderJni.getSeekPosition(timeUs); + if (result.length == 4) { + return new SeekPoints(new SeekPoint(result[0], result[1]), new SeekPoint(result[2], result[3])); + } else { + return new SeekPoints(new SeekPoint(timeUs, decoderJni.getDecodePosition())); + } } @Override diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index d60a7cead2..0bea30752a 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -200,9 +200,13 @@ DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) { return context->parser->getNextFrameFirstSampleIndex(); } -DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { +DECODER_FUNC(jlongArray, flacGetSeekPosition, jlong jContext, jlong timeUs) { Context *context = reinterpret_cast(jContext); - return context->parser->getSeekPosition(timeUs); + int64_t *result = context->parser->getSeekPosition(timeUs); + const jlong resultJLong[4] = {result[0], result[1], result[2], result[3]}; + jlongArray resultArray = env->NewLongArray(4); + env->SetLongArrayRegion(resultArray, 0, 4, resultJLong); + return resultArray; } DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 830f3e2178..dc0a685951 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -438,9 +438,11 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { return bufferSize; } -int64_t FLACParser::getSeekPosition(int64_t timeUs) { +int64_t* FLACParser::getSeekPosition(int64_t timeUs) { + int64_t *result = new int64_t[4]; + memset(result, -1, sizeof(result)); if (!mSeekTable) { - return -1; + return result; } int64_t sample = (timeUs * getSampleRate()) / 1000000LL; @@ -452,8 +454,21 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) { for (unsigned i = mSeekTable->num_points; i > 0; ) { i--; if (points[i].sample_number <= sample) { - return firstFrameOffset + points[i].stream_offset; + result[0] = points[i].sample_number / getSampleRate() * 1000000LL; + result[1] = firstFrameOffset + points[i].stream_offset; + if (i + 1 <= mSeekTable->num_points) { + result[2] = points[i + 1].sample_number / getSampleRate() * 1000000LL; + result[3] = firstFrameOffset + points[i + 1].stream_offset; + } else { + result[2] = result[0]; + result[3] = result[1]; + } + return result; } } - return firstFrameOffset; + result[0] = timeUs; + result[1] = firstFrameOffset; + result[2] = timeUs; + result[3] = firstFrameOffset; + return result; } diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index fd3e36a806..ac6e3e26d8 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -84,7 +84,7 @@ class FLACParser { bool decodeMetadata(); size_t readBuffer(void *output, size_t output_size); - int64_t getSeekPosition(int64_t timeUs); + int64_t* getSeekPosition(int64_t timeUs); void flush() { reset(mCurrentPos); From 33ef4184e8dbc9e6aa818670f11aa76706b1e458 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 2 Sep 2019 10:15:12 +0100 Subject: [PATCH 370/807] Publish test utils modules as release artificats. This allows external users to easily write unit tests involving ExoPlayer instances. Issue:#6267 PiperOrigin-RevId: 266741790 --- RELEASENOTES.md | 2 ++ testutils/README.md | 10 ++++++++++ testutils/build.gradle | 11 +++++++++++ .../android/exoplayer2/testutil/ActionSchedule.java | 6 +++--- .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 testutils/README.md diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ef6eedbe1c..1284e8febb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -56,6 +56,8 @@ [#4249](https://github.com/google/ExoPlayer/issues/4249), [#4319](https://github.com/google/ExoPlayer/issues/4319), [#4337](https://github.com/google/ExoPlayer/issues/4337)). +* Publish `testutils` module to simplify unit testing with ExoPlayer + ([#6267](https://github.com/google/ExoPlayer/issues/6267)). ### 2.10.4 ### diff --git a/testutils/README.md b/testutils/README.md new file mode 100644 index 0000000000..d1ab6b1af5 --- /dev/null +++ b/testutils/README.md @@ -0,0 +1,10 @@ +# ExoPlayer test utils # + +Provides utility classes for ExoPlayer unit and instrumentation tests. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.testutil` + belong to this module. + +[Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/testutils/build.gradle b/testutils/build.gradle index b5e68187be..5c6731c17b 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -49,3 +49,14 @@ dependencies { testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } + +ext { + javadocTitle = 'Test utils' +} +apply from: '../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-testutils' + releaseDescription = 'Test utils for ExoPlayer.' +} +apply from: '../publish.gradle' 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 1b0a00624e..e8abfdb73f 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 @@ -311,12 +311,12 @@ public final class ActionSchedule { /** * Schedules a new source preparation action to be executed. - * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean). * + * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean) * @return The builder, for convenience. */ - public Builder prepareSource(MediaSource mediaSource, boolean resetPosition, - boolean resetState) { + public Builder prepareSource( + MediaSource mediaSource, boolean resetPosition, boolean resetState) { return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); } 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 bcb97be287..6d141fe04a 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 @@ -34,7 +34,7 @@ import java.util.List; /** * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting a - * track will give the player a {@link ChunkSampleStream}. + * track will give the player a {@link ChunkSampleStream}. */ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod implements SequenceableLoader.Callback> { From d9042a2985c2b7cb3e84bcd6192ed7d91ceadee7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 2 Sep 2019 10:53:18 +0100 Subject: [PATCH 371/807] Move effectively private method further down. @VisibleForTesting sets the actual visiblity to private (except for tests), so the method should be further down in code. PiperOrigin-RevId: 266746628 --- .../exoplayer2/upstream/DefaultHttpDataSource.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 0cf54a153c..c5b3c0b08b 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 @@ -381,13 +381,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } - /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */ - @VisibleForTesting - /* package */ - HttpURLConnection openConnection(URL url) throws IOException { - return (HttpURLConnection) url.openConnection(); - } - /** * Returns the current connection, or null if the source is not currently opened. * @@ -568,6 +561,12 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou return connection; } + /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */ + @VisibleForTesting + /* package */ HttpURLConnection openConnection(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + /** * Handles a redirect. * From aff9e731b253a8ad65f06f5b6aa62944d2c4af4c Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 2 Sep 2019 11:46:14 +0100 Subject: [PATCH 372/807] Move DefaultHttpDataSource request header explanation to class Javadoc. The Javadoc for open() won't be read by anyone because it's an overridden method of the interface and not called directly from application code. Move doc to class documentation as this is a generic explanation about the functionality of the class. Also added "all" to clarify that all the parameters will be added and the order of preference just applies in case of key clashes. PiperOrigin-RevId: 266753249 --- .../exoplayer2/upstream/DefaultHttpDataSource.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 c5b3c0b08b..37329a4868 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 @@ -52,6 +52,10 @@ import java.util.zip.GZIPInputStream; * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link * #DefaultHttpDataSource(String, Predicate, int, int, boolean, RequestProperties)} constructor and * passing {@code true} as the second last argument. + * + *

        Note: HTTP request headers will be set using all parameters passed via (in order of decreasing + * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to + * construct the instance. */ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { @@ -265,10 +269,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** * Opens the source to read the specified data. - * - *

        Note: HTTP request headers will be set using parameters passed via (in order of decreasing - * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to - * construct the instance. */ @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { From 82d10e2ea8baf001679140e75acc773ec63b5edc Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 2 Sep 2019 13:43:42 +0100 Subject: [PATCH 373/807] Bypass sniffing for single extractor Sniffing is performed in ProgressiveMediaPeriod even if a single extractor is provided. Skip it in that case to improve performances. Issue:#6325 PiperOrigin-RevId: 266766373 --- RELEASENOTES.md | 2 ++ .../source/ProgressiveMediaPeriod.java | 33 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1284e8febb..03b3199e21 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is + provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)). * Surface information provided by methods `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` added in Android Q in `MediaCodecInfo` class ([#5839](https://github.com/google/ExoPlayer/issues/5839)). 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 d25fff5104..41fefafab7 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 @@ -1068,21 +1068,28 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (extractor != null) { return extractor; } - for (Extractor extractor : extractors) { - try { - if (extractor.sniff(input)) { - this.extractor = extractor; - break; + if (extractors.length == 1) { + this.extractor = extractors[0]; + } else { + for (Extractor extractor : extractors) { + try { + if (extractor.sniff(input)) { + this.extractor = extractor; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); } - } catch (EOFException e) { - // Do nothing. - } finally { - input.resetPeekPosition(); } - } - if (extractor == null) { - throw new UnrecognizedInputFormatException("None of the available extractors (" - + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri); + if (extractor == null) { + throw new UnrecognizedInputFormatException( + "None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + + ") could read the stream.", + uri); + } } extractor.init(output); return extractor; From 494b6f6f3bcc854ab4d224c426b5d6b3546623e8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 2 Sep 2019 14:35:44 +0100 Subject: [PATCH 374/807] Re-use local variable in replacement of unnecessary indirections PiperOrigin-RevId: 266772364 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 10827f8aa7..f73dde58c3 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 @@ -319,7 +319,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (!MimeTypes.isVideo(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; } - DrmInitData drmInitData = format.drmInitData; + @Nullable DrmInitData drmInitData = format.drmInitData; // Assume encrypted content requires secure decoders. boolean requiresSecureDecryption = drmInitData != null; List decoderInfos = @@ -341,10 +341,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return FORMAT_UNSUPPORTED_SUBTYPE; } boolean supportsFormatDrm = - format.drmInitData == null + drmInitData == null || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) || (format.exoMediaCryptoType == null - && supportsFormatDrm(drmSessionManager, format.drmInitData)); + && supportsFormatDrm(drmSessionManager, drmInitData)); if (!supportsFormatDrm) { return FORMAT_UNSUPPORTED_DRM; } From eedf50fdcad5263fa3509db2fc702f79b88f0810 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 2 Sep 2019 16:05:31 +0100 Subject: [PATCH 375/807] use isPlaying to determine which notification action to display in compact view PiperOrigin-RevId: 266782250 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 8ad00e1dda..e4fcb37af3 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 @@ -1182,10 +1182,10 @@ public class PlayerNotificationManager { if (skipPreviousActionIndex != -1) { actionIndices[actionCounter++] = skipPreviousActionIndex; } - boolean playWhenReady = player.getPlayWhenReady(); - if (pauseActionIndex != -1 && playWhenReady) { + boolean isPlaying = isPlaying(player); + if (pauseActionIndex != -1 && isPlaying) { actionIndices[actionCounter++] = pauseActionIndex; - } else if (playActionIndex != -1 && !playWhenReady) { + } else if (playActionIndex != -1 && !isPlaying) { actionIndices[actionCounter++] = playActionIndex; } if (skipNextActionIndex != -1) { From bcd7de5316ff4625edd4c80f97b29d6c89d4d1bc Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Sep 2019 17:13:57 +0100 Subject: [PATCH 376/807] Fix exception message PiperOrigin-RevId: 266790267 --- .../java/com/google/android/exoplayer2/util/AtomicFile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index f2259e8f1a..fa40f0f012 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -107,8 +107,9 @@ public final class AtomicFile { } catch (FileNotFoundException e) { File parent = baseName.getParentFile(); if (parent == null || !parent.mkdirs()) { - throw new IOException("Couldn't create directory " + baseName, e); + throw new IOException("Couldn't create " + baseName, e); } + // Try again now that we've created the parent directory. try { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e2) { From d2c056eb91614aecca00345380ffa308195069ba Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 2 Sep 2019 18:05:18 +0100 Subject: [PATCH 377/807] move transparency of shuffle mode off button to bitmap PiperOrigin-RevId: 266795413 --- .../exoplayer2/ui/PlayerControlView.java | 13 ++++++--- .../exo_controls_shuffle_off.xml | 26 ++++++++++++++++++ ...huffle.xml => exo_controls_shuffle_on.xml} | 0 .../exo_controls_shuffle_off.png | Bin 0 -> 265 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 182 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 228 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 342 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin .../exo_controls_shuffle_off.png | Bin 0 -> 438 bytes ...huffle.png => exo_controls_shuffle_on.png} | Bin library/ui/src/main/res/values/styles.xml | 2 +- 14 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_shuffle.xml => exo_controls_shuffle_on.xml} (100%) create mode 100644 library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-hdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-ldpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-mdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) create mode 100644 library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_shuffle.png => exo_controls_shuffle_on.png} (100%) 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 0765105314..e938192d8e 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 @@ -238,7 +238,7 @@ public class PlayerControlView extends FrameLayout { private final View fastForwardButton; private final View rewindButton; private final ImageView repeatToggleButton; - private final View shuffleButton; + private final ImageView shuffleButton; private final View vrButton; private final TextView durationView; private final TextView positionView; @@ -256,6 +256,8 @@ public class PlayerControlView extends FrameLayout { private final String repeatOffButtonContentDescription; private final String repeatOneButtonContentDescription; private final String repeatAllButtonContentDescription; + private final Drawable shuffleOnButtonDrawable; + private final Drawable shuffleOffButtonDrawable; @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; @@ -407,6 +409,8 @@ public class PlayerControlView extends FrameLayout { repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_on); + shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_off); repeatOffButtonContentDescription = resources.getString(R.string.exo_controls_repeat_off_description); repeatOneButtonContentDescription = @@ -817,10 +821,11 @@ public class PlayerControlView extends FrameLayout { shuffleButton.setVisibility(GONE); } else if (player == null) { setButtonEnabled(false, shuffleButton); + shuffleButton.setImageDrawable(shuffleOffButtonDrawable); } else { - shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); - shuffleButton.setEnabled(true); - shuffleButton.setVisibility(VISIBLE); + setButtonEnabled(true, shuffleButton); + shuffleButton.setImageDrawable( + player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable); } } diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml new file mode 100644 index 0000000000..283ce30c3c --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..b693422db75eeef7c4beded7dbe9644eec85194a GIT binary patch literal 265 zcmV+k0rvihP)|OH=gIq{2%7koSSbot2;Bl*Z~rsziFwQQA0p}?-;bZ zA$d>OouIrYwyU7LBR<=|u)HG-;CVvud^j-CJO!?Lkvsv6n0USp%K gy{w(*k4Ut36#DLn;`P78pse{J9Po?zX{o-TQ{ZXM3MDneg;gita;L_f`?^_1 zl0MUbyBlW$9RnGj!Vh4CdW#`zCpT cnglDurOSd!uTxH$0iDg@>FVdQ&MBb@03C8#m;e9( literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..2b67cabf5afabec4433cb71905087d42cc094b89 GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7`%AFv@zmIEGX(zP-`Nb|^uj^TTd7ZCALw$Y0r&=p1Lj{YUe@tcqam3F*7V$q%g~zq$ zeqNj>wf&6!pX1j{=FJoT_W#WMlIPD^eY0-b{9hz|?fXTsbN|)Wd}85v(sM?NtyIO*{-%sBUL`7Ldk h!^?}awnnY}#`f#r3*~;M_+_B*@^tlcS?83{1OVHAnMMEr literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..22209d1f88712f3b615018fb1e7cfe572bdf256c GIT binary patch literal 438 zcmV;n0ZIOeP)A95OS2(`R-JKs2G;M#V*4;bHWx~}WGuIsw4 z>;9VVwAF*pWZGz)`kI&jrlF6^6K3X*1YvG|fG{;bK;Qtq`mz9l1N5vzkiY>E0ssUE zTI+xT0fO`Q8XOQJIG+Oo1m|-AC-8g@-~^tJ0f4~s_j=Nn0ssN$uaEEDng9TRfbx$s z0RRAj<(ptYfWY!iFd#ty`6eI$Agoxv2><{H8=h~1Pv;($!Pf3`RvnSWluE|5PJu+n=jp94&Y From d37c18abfe6cbfc869a91e71f7bff7add9a68128 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 2 Sep 2019 18:28:20 +0100 Subject: [PATCH 378/807] Clarify LoadErrorHandlingPolicy's loadDurationMs javadocs PiperOrigin-RevId: 266797383 --- .../exoplayer2/upstream/LoadErrorHandlingPolicy.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java index 3432935d5f..293d1e7510 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -44,8 +44,8 @@ public interface LoadErrorHandlingPolicy { * * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to * load. - * @param loadDurationMs The duration in milliseconds of the load up to the point at which the - * error occurred, including any previous attempts. + * @param loadDurationMs The duration in milliseconds of the load from the start of the first load + * attempt up to the point at which the error occurred. * @param exception The load error. * @param errorCount The number of errors this load has encountered, including this one. * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should @@ -64,8 +64,8 @@ public interface LoadErrorHandlingPolicy { * * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to * load. - * @param loadDurationMs The duration in milliseconds of the load up to the point at which the - * error occurred, including any previous attempts. + * @param loadDurationMs The duration in milliseconds of the load from the start of the first load + * attempt up to the point at which the error occurred. * @param exception The load error. * @param errorCount The number of errors this load has encountered, including this one. * @return The number of milliseconds to wait before attempting the load again, or {@link From 2d0b10a73a511ed38f1e48d51caf8feaf7065286 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Sep 2019 19:19:00 +0100 Subject: [PATCH 379/807] Use constant to define androidx annotation version PiperOrigin-RevId: 266801762 --- constants.gradle | 3 ++- demos/gvr/build.gradle | 2 +- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 4 ++-- extensions/gvr/build.gradle | 2 +- extensions/ima/build.gradle | 2 +- extensions/leanback/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- extensions/opus/build.gradle | 6 +++--- extensions/rtmp/build.gradle | 2 +- extensions/vp9/build.gradle | 6 +++--- library/core/build.gradle | 10 +++++----- library/dash/build.gradle | 2 +- library/hls/build.gradle | 2 +- library/smoothstreaming/build.gradle | 2 +- library/ui/build.gradle | 2 +- playbacktests/build.gradle | 6 +++--- testutils/build.gradle | 6 +++--- 22 files changed, 36 insertions(+), 35 deletions(-) diff --git a/constants.gradle b/constants.gradle index 9510b8442e..77609df69b 100644 --- a/constants.gradle +++ b/constants.gradle @@ -26,7 +26,8 @@ project.ext { checkerframeworkVersion = '2.5.0' jsr305Version = '3.0.2' kotlinAnnotationsVersion = '1.3.31' - androidXTestVersion = '1.1.0' + androidxAnnotationVersion = '1.1.0' + androidxTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { diff --git a/demos/gvr/build.gradle b/demos/gvr/build.gradle index 37d8fbbb99..96c699b2e6 100644 --- a/demos/gvr/build.gradle +++ b/demos/gvr/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'extension-gvr') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 124555d9b5..289fa1dc83 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.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index be08ca9ea2..06c734986c 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,7 +62,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.material:material:1.0.0' implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 4af8f94c58..0d7d96db4c 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api 'com.google.android.gms:play-services-cast-framework:17.0.0' - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 9c49ba94e1..9b618cd036 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -33,7 +33,7 @@ android { dependencies { api 'org.chromium.net:cronet-embedded:75.3770.101' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils') diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 2b5a6010a9..657fa75c24 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index dfac2e1c26..5d68711aa7 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,10 +39,10 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') - androidTestImplementation 'androidx.test:runner:' + androidXTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 1031d6f4b7..f8992616a2 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.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index e330b13e47..e2292aed8f 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,7 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index ecaa78e25b..f0be172c90 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.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.leanback:leanback:1.0.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 68bd422185..b0d9fb0187 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.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion api 'com.squareup.okhttp3:okhttp:3.12.1' } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 7b621a8df9..2759299d63 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,11 +39,11 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion - androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestVersion + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion } ext { diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index 9c709305bf..88d3524d72 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.1.0' - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 3b8271869b..e40d6e02d7 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,11 +39,11 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion - androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestVersion + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion } diff --git a/library/core/build.gradle b/library/core/build.gradle index d6aee8a35d..afccef3e24 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -57,21 +57,21 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion - androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestVersion + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion - testImplementation 'androidx.test:core:' + androidXTestVersion - testImplementation 'androidx.test.ext:junit:' + androidXTestVersion + testImplementation 'androidx.test:core:' + androidxTestVersion + testImplementation 'androidx.test.ext:junit:' + androidxTestVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/library/dash/build.gradle b/library/dash/build.gradle index c64da2b86d..ac90d64c1e 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -42,7 +42,7 @@ dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 0f685c1130..07696c1c26 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index b16157f49b..4fe2fae328 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -42,7 +42,7 @@ dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 5b3123e302..3735c94096 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.1' - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 5865d3c36d..d77944cf23 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -32,9 +32,9 @@ android { } dependencies { - androidTestImplementation 'androidx.test:rules:' + androidXTestVersion - androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.1.0' + androidTestImplementation 'androidx.test:rules:' + androidxTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestVersion + androidTestImplementation 'androidx.annotation:annotation:' + androidxAnnotationVersion androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/testutils/build.gradle b/testutils/build.gradle index 5c6731c17b..3c7b13a6a8 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -39,10 +39,10 @@ android { dependencies { api 'org.mockito:mockito-core:' + mockitoVersion - api 'androidx.test:core:' + androidXTestVersion - api 'androidx.test.ext:junit:' + androidXTestVersion + api 'androidx.test:core:' + androidxTestVersion + api 'androidx.test.ext:junit:' + androidxTestVersion api 'com.google.truth:truth:' + truthVersion - implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion From a564c8011fc7e53dcc875c48a991bee94eef2ef4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Sep 2019 19:29:36 +0100 Subject: [PATCH 380/807] Remove seemingly unused dependency PiperOrigin-RevId: 266802447 --- demos/cast/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 60b9cdbc4e..ea058d95d4 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -57,7 +57,6 @@ dependencies { implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'com.google.android.material:material:1.0.0' } From 0dc997103b5c4fe37bf2f87072ee8c538da6ed1c Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Sep 2019 19:31:28 +0100 Subject: [PATCH 381/807] Use constant to define androidx media version PiperOrigin-RevId: 266802551 --- constants.gradle | 1 + extensions/mediasession/build.gradle | 2 +- library/ui/build.gradle | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/constants.gradle b/constants.gradle index 77609df69b..61f31ca35e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -27,6 +27,7 @@ project.ext { jsr305Version = '3.0.2' kotlinAnnotationsVersion = '1.3.31' androidxAnnotationVersion = '1.1.0' + androidxMediaVersion = '1.0.1' androidxTestVersion = '1.1.0' truthVersion = '0.44' modulePrefix = ':' diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 7ee973723c..537c5ba534 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - api 'androidx.media:media:1.0.1' + api 'androidx.media:media:' + androidxMediaVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 3735c94096..509dd22a28 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -40,7 +40,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.media:media:1.0.1' + api 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') From bb89f69bbbe3595dcd7f494648b9b705e9f618a1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Sep 2019 19:44:54 +0100 Subject: [PATCH 382/807] Migrate extensions to androidx.collection PiperOrigin-RevId: 266803466 --- constants.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/constants.gradle b/constants.gradle index 61f31ca35e..69726d9ed8 100644 --- a/constants.gradle +++ b/constants.gradle @@ -27,6 +27,7 @@ project.ext { jsr305Version = '3.0.2' kotlinAnnotationsVersion = '1.3.31' androidxAnnotationVersion = '1.1.0' + androidxCollectionVersion = '1.1.0' androidxMediaVersion = '1.0.1' androidxTestVersion = '1.1.0' truthVersion = '0.44' From 52edbaa9a327abe285a47b6fa6ed452188d1d7c2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Sep 2019 19:51:17 +0100 Subject: [PATCH 383/807] Update cronet and workmanager dependencies PiperOrigin-RevId: 266803888 --- extensions/cronet/build.gradle | 2 +- extensions/workmanager/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 9b618cd036..d5b7a99f96 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:75.3770.101' + api 'org.chromium.net:cronet-embedded:76.3809.111' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index ea7564316f..6a7aa10722 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.1.0' + implementation 'androidx.work:work-runtime:2.2.0' } ext { From 64829a0373b73870f52549ede17197abfa860b72 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 2 Sep 2019 21:29:09 +0100 Subject: [PATCH 384/807] Calculate loadDurationMs in DefaultDrmSession key and provisioning requests PiperOrigin-RevId: 266812110 --- .../exoplayer2/drm/DefaultDrmSession.java | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) 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 a667c8374a..23c5f0a755 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 @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -112,12 +113,12 @@ public class DefaultDrmSession implements DrmSession implements DrmSession implements DrmSession implements DrmSession implements DrmSession implements DrmSession implements DrmSession implements DrmSession implements DrmSession loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) { + requestTask.errorCount++; + if (requestTask.errorCount + > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) { return false; } Message retryMsg = Message.obtain(originalMsg); - retryMsg.arg2 = errorCount; IOException ioException = e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e); - // TODO: Add loadDurationMs calculation before allowing user-provided load error handling - // policies. long retryDelayMs = loadErrorHandlingPolicy.getRetryDelayMsFor( - C.DATA_TYPE_DRM, /* loadDurationMs= */ C.TIME_UNSET, ioException, errorCount); + C.DATA_TYPE_DRM, + /* loadDurationMs= */ SystemClock.elapsedRealtime() - requestTask.startTimeMs, + ioException, + requestTask.errorCount); sendMessageDelayed(retryMsg, retryDelayMs); return true; } } + + private static final class RequestTask { + + public final boolean allowRetry; + public final long startTimeMs; + public final Object request; + public int errorCount; + + public RequestTask(boolean allowRetry, long startTimeMs, Object request) { + this.allowRetry = allowRetry; + this.startTimeMs = startTimeMs; + this.request = request; + } + } } From e4eb6b7ea90cf273591f47a9ed19f2439d74a8f7 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Sep 2019 09:49:32 +0100 Subject: [PATCH 385/807] move transparency values of buttons to resources to make it accessible for customization PiperOrigin-RevId: 266880069 --- .../android/exoplayer2/ui/PlayerControlView.java | 11 ++++++++++- library/ui/src/main/res/values/constants.xml | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) 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 e938192d8e..ceb131cc29 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 @@ -258,6 +258,8 @@ public class PlayerControlView extends FrameLayout { private final String repeatAllButtonContentDescription; private final Drawable shuffleOnButtonDrawable; private final Drawable shuffleOffButtonDrawable; + private final float buttonAlphaEnabled; + private final float buttonAlphaDisabled; @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; @@ -405,7 +407,14 @@ public class PlayerControlView extends FrameLayout { } vrButton = findViewById(R.id.exo_vr); setShowVrButton(false); + Resources resources = context.getResources(); + + buttonAlphaEnabled = + (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100; + buttonAlphaDisabled = + (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100; + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); @@ -959,7 +968,7 @@ public class PlayerControlView extends FrameLayout { return; } view.setEnabled(enabled); - view.setAlpha(enabled ? 1f : 0.3f); + view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled); view.setVisibility(VISIBLE); } diff --git a/library/ui/src/main/res/values/constants.xml b/library/ui/src/main/res/values/constants.xml index 9b374d8382..9bd616583e 100644 --- a/library/ui/src/main/res/values/constants.xml +++ b/library/ui/src/main/res/values/constants.xml @@ -18,6 +18,9 @@ 71dp 52dp + 100 + 33 + #AA000000 #FFF4F3F0 From a12c6641d9a3c49a6d36ab89da7ac6888aedcfa6 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Sep 2019 10:17:27 +0100 Subject: [PATCH 386/807] provide content description for shuffle on/off button PiperOrigin-RevId: 266884166 --- .../android/exoplayer2/ui/PlayerControlView.java | 12 ++++++++++++ .../res/layout/exo_playback_control_view.xml | 2 +- library/ui/src/main/res/values-af/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-am/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ar/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-az/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-b+sr+Latn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-be/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-bg/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-bn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-bs/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ca/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-cs/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-da/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-de/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-el/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-en-rAU/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-en-rGB/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-en-rIN/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-es-rUS/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-es/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-et/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-eu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-fa/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-fi/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-fr-rCA/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-fr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-gl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-gu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hi/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-hy/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-in/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-is/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-it/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-iw/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ja/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ka/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-kk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-km/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-kn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ko/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ky/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-lo/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-lt/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-lv/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-mk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ml/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-mn/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-mr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ms/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-my/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-nb/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ne/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-nl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-pa/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-pl/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-pt-rPT/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-pt/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ro/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ru/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-si/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sq/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sv/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-sw/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ta/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-te/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-th/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-tl/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-tr/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-uk/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-ur/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-uz/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-vi/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-zh-rCN/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-zh-rHK/strings.xml | 16 +++++++++++++++- .../ui/src/main/res/values-zh-rTW/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values-zu/strings.xml | 16 +++++++++++++++- library/ui/src/main/res/values/strings.xml | 6 ++++-- library/ui/src/main/res/values/styles.xml | 5 ----- 84 files changed, 1217 insertions(+), 88 deletions(-) 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 ceb131cc29..fd949db6a2 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 @@ -260,6 +260,8 @@ public class PlayerControlView extends FrameLayout { private final Drawable shuffleOffButtonDrawable; private final float buttonAlphaEnabled; private final float buttonAlphaDisabled; + private final String shuffleOnContentDescription; + private final String shuffleOffContentDescription; @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; @@ -426,6 +428,9 @@ public class PlayerControlView extends FrameLayout { resources.getString(R.string.exo_controls_repeat_one_description); repeatAllButtonContentDescription = resources.getString(R.string.exo_controls_repeat_all_description); + shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description); + shuffleOffContentDescription = + resources.getString(R.string.exo_controls_shuffle_off_description); } @SuppressWarnings("ResourceType") @@ -800,6 +805,8 @@ public class PlayerControlView extends FrameLayout { } if (player == null) { setButtonEnabled(false, repeatToggleButton); + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); return; } setButtonEnabled(true, repeatToggleButton); @@ -831,10 +838,15 @@ public class PlayerControlView extends FrameLayout { } else if (player == null) { setButtonEnabled(false, shuffleButton); shuffleButton.setImageDrawable(shuffleOffButtonDrawable); + shuffleButton.setContentDescription(shuffleOffContentDescription); } else { setButtonEnabled(true, shuffleButton); shuffleButton.setImageDrawable( player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable); + shuffleButton.setContentDescription( + player.getShuffleModeEnabled() + ? shuffleOnContentDescription + : shuffleOffContentDescription); } } 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 027e57ee92..acfddf1146 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 @@ -37,7 +37,7 @@ style="@style/ExoMediaButton.Rewind"/> + style="@style/ExoMediaButton"/> diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 8a983c543a..fa630292a9 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -1,4 +1,18 @@ + Vorige snit Volgende snit @@ -10,7 +24,7 @@ Herhaal niks Herhaal een Herhaal alles - Skommel + Skommel Volskermmodus VR-modus Aflaai diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index f56a6c06bf..b754aa90ea 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -1,4 +1,18 @@ + ቀዳሚ ትራክ ቀጣይ ትራክ @@ -10,7 +24,7 @@ ምንም አትድገም አንድ ድገም ሁሉንም ድገም - በውዝ + በውዝ የሙሉ ማያ ሁነታ የቪአር ሁነታ አውርድ diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index 91063e1a54..87cad1be25 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -1,4 +1,18 @@ + المقطع الصوتي السابق المقطع الصوتي التالي @@ -10,7 +24,7 @@ عدم التكرار تكرار مقطع صوتي واحد تكرار الكل - ترتيب عشوائي + ترتيب عشوائي وضع ملء الشاشة وضع VR تنزيل diff --git a/library/ui/src/main/res/values-az/strings.xml b/library/ui/src/main/res/values-az/strings.xml index 0f5fbe3f4d..ddfc653731 100644 --- a/library/ui/src/main/res/values-az/strings.xml +++ b/library/ui/src/main/res/values-az/strings.xml @@ -1,4 +1,18 @@ + Əvvəlki trek Növbəti trek @@ -10,7 +24,7 @@ Heç biri təkrarlanmasın Biri təkrarlansın Hamısı təkrarlansın - Qarışdırın + Qarışdırın Tam ekran rejimi VR rejimi Endirin diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index 16300747f7..73c4223d8c 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -1,4 +1,18 @@ + Prethodna pesma Sledeća pesma @@ -10,7 +24,7 @@ Ne ponavljaj nijednu Ponovi jednu Ponovi sve - Pusti nasumično + Pusti nasumično Režim celog ekrana VR režim Preuzmi diff --git a/library/ui/src/main/res/values-be/strings.xml b/library/ui/src/main/res/values-be/strings.xml index 6a33be2a8f..7187494eca 100644 --- a/library/ui/src/main/res/values-be/strings.xml +++ b/library/ui/src/main/res/values-be/strings.xml @@ -1,4 +1,18 @@ + Папярэдні трэк Наступны трэк @@ -10,7 +24,7 @@ Не паўтараць нічога Паўтарыць адзін элемент Паўтарыць усе - Перамяшаць + Перамяшаць Поўнаэкранны рэжым VR-рэжым Спампаваць diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index 511a5e4f19..f7dcd29e49 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -1,4 +1,18 @@ + Предишен запис Следващ запис @@ -10,7 +24,7 @@ Без повтаряне Повтаряне на един елемент Повтаряне на всички - Разбъркване + Разбъркване Режим на цял екран режим за VR Изтегляне diff --git a/library/ui/src/main/res/values-bn/strings.xml b/library/ui/src/main/res/values-bn/strings.xml index cca445feca..6ccd22744c 100644 --- a/library/ui/src/main/res/values-bn/strings.xml +++ b/library/ui/src/main/res/values-bn/strings.xml @@ -1,4 +1,18 @@ + আগের ট্র্যাক পরবর্তী ট্র্যাক @@ -10,7 +24,7 @@ কোনও আইটেম আবার চালাবেন না একটি আইটেম আবার চালান সবগুলি আইটেম আবার চালান - শাফেল করুন + শাফেল করুন পূর্ণ স্ক্রিন মোড ভিআর মোড ডাউনলোড করুন diff --git a/library/ui/src/main/res/values-bs/strings.xml b/library/ui/src/main/res/values-bs/strings.xml index 24fb7b2b3b..a9a960285f 100644 --- a/library/ui/src/main/res/values-bs/strings.xml +++ b/library/ui/src/main/res/values-bs/strings.xml @@ -1,4 +1,18 @@ + Prethodna numera Sljedeća numera @@ -10,7 +24,7 @@ Ne ponavljaj Ponovi jedno Ponovi sve - Izmiješaj + Izmiješaj Način rada preko cijelog ekrana VR način rada Preuzmi diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index 3b48eab3b8..39a3ce85c9 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Pista següent @@ -10,7 +24,7 @@ No en repeteixis cap Repeteix una Repeteix tot - Reprodueix aleatòriament + Reprodueix aleatòriament Mode de pantalla completa Mode RV Baixa diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 1568074f9f..1ad837b32d 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -1,4 +1,18 @@ + Předchozí skladba Další skladba @@ -10,7 +24,7 @@ Neopakovat Opakovat jednu Opakovat vše - Náhodně + Náhodně Režim celé obrazovky Režim VR Stáhnout diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index 19b0f09446..2bef98e781 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -1,4 +1,18 @@ + Afspil forrige Afspil næste @@ -10,7 +24,7 @@ Gentag ingen Gentag én Gentag alle - Bland + Bland Fuld skærm VR-tilstand Download diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index 1bb620dd2b..e06459de8d 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -1,4 +1,18 @@ + Vorheriger Titel Nächster Titel @@ -10,7 +24,7 @@ Keinen wiederholen Einen wiederholen Alle wiederholen - Zufallsmix + Zufallsmix Vollbildmodus VR-Modus Herunterladen diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 1ddbe4a5fa..47144dc00c 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -1,4 +1,18 @@ + Προηγούμενο κομμάτι Επόμενο κομμάτι @@ -10,7 +24,7 @@ Καμία επανάληψη Επανάληψη ενός κομματιού Επανάληψη όλων - Τυχαία αναπαραγωγή + Τυχαία αναπαραγωγή Λειτουργία πλήρους οθόνης Λειτουργία VR mode Λήψη diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index cf25e2ada0..62125c5226 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -1,4 +1,18 @@ + Previous track Next track @@ -10,7 +24,7 @@ Repeat none Repeat one Repeat all - Shuffle + Shuffle Full-screen mode VR mode Download diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index cf25e2ada0..62125c5226 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -1,4 +1,18 @@ + Previous track Next track @@ -10,7 +24,7 @@ Repeat none Repeat one Repeat all - Shuffle + Shuffle Full-screen mode VR mode Download diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index cf25e2ada0..62125c5226 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -1,4 +1,18 @@ + Previous track Next track @@ -10,7 +24,7 @@ Repeat none Repeat one Repeat all - Shuffle + Shuffle Full-screen mode VR mode Download diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index ceeb0b8497..beeeba4e9c 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Pista siguiente @@ -10,7 +24,7 @@ No repetir Repetir uno Repetir todo - Reproducir aleatoriamente + Reproducir aleatoriamente Modo de pantalla completa Modo RV Descargar diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 0118da57be..e880d66bf0 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Siguiente pista @@ -10,7 +24,7 @@ No repetir Repetir uno Repetir todo - Reproducir aleatoriamente + Reproducir aleatoriamente Modo de pantalla completa Modo RV Descargar diff --git a/library/ui/src/main/res/values-et/strings.xml b/library/ui/src/main/res/values-et/strings.xml index 99ca9548ed..515c665181 100644 --- a/library/ui/src/main/res/values-et/strings.xml +++ b/library/ui/src/main/res/values-et/strings.xml @@ -1,4 +1,18 @@ + Eelmine lugu Järgmine lugu @@ -10,7 +24,7 @@ Ära korda ühtegi Korda ühte Korda kõiki - Esita juhuslikus järjekorras + Esita juhuslikus järjekorras Täisekraani režiim VR-režiim Allalaadimine diff --git a/library/ui/src/main/res/values-eu/strings.xml b/library/ui/src/main/res/values-eu/strings.xml index 4d992fee0f..3f3d75d4f8 100644 --- a/library/ui/src/main/res/values-eu/strings.xml +++ b/library/ui/src/main/res/values-eu/strings.xml @@ -1,4 +1,18 @@ + Aurreko pista Hurrengo pista @@ -10,7 +24,7 @@ Ez errepikatu Errepikatu bat Errepikatu guztiak - Erreproduzitu ausaz + Erreproduzitu ausaz Pantaila osoko modua EB modua Deskargak diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index fed94b5569..dfc74a9c21 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -1,4 +1,18 @@ + آهنگ قبلی آهنگ بعدی @@ -10,7 +24,7 @@ تکرار هیچ‌کدام یکبار تکرار تکرار همه - درهم + درهم حالت تمام‌صفحه حالت واقعیت مجازی بارگیری diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 0dc2f9d346..c0e53c437b 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -1,4 +1,18 @@ + Edellinen kappale Seuraava kappale @@ -10,7 +24,7 @@ Ei uudelleentoistoa Toista yksi uudelleen Toista kaikki uudelleen - Satunnaistoisto + Satunnaistoisto Koko näytön tila VR-tila Lataa diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index 0f3534924f..ef42066df3 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -1,4 +1,18 @@ + Chanson précédente Chanson suivante @@ -10,7 +24,7 @@ Ne rien lire en boucle Lire une chanson en boucle Tout lire en boucle - Lecture aléatoire + Lecture aléatoire Mode Plein écran Mode RV Télécharger diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index 46c07f531e..057a6a8f67 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -1,4 +1,18 @@ + Titre précédent Titre suivant @@ -10,7 +24,7 @@ Ne rien lire en boucle Lire un titre en boucle Tout lire en boucle - Aléatoire + Aléatoire Mode plein écran Mode RV Télécharger diff --git a/library/ui/src/main/res/values-gl/strings.xml b/library/ui/src/main/res/values-gl/strings.xml index e6689353f1..419ea0c552 100644 --- a/library/ui/src/main/res/values-gl/strings.xml +++ b/library/ui/src/main/res/values-gl/strings.xml @@ -1,4 +1,18 @@ + Pista anterior Pista seguinte @@ -10,7 +24,7 @@ Non repetir Repetir unha pista Repetir todas as pistas - Reprodución aleatoria + Reprodución aleatoria Modo de pantalla completa Modo RV Descargar diff --git a/library/ui/src/main/res/values-gu/strings.xml b/library/ui/src/main/res/values-gu/strings.xml index 488eb39f6a..daec2b447d 100644 --- a/library/ui/src/main/res/values-gu/strings.xml +++ b/library/ui/src/main/res/values-gu/strings.xml @@ -1,4 +1,18 @@ + પહેલાંનો ટ્રૅક આગલો ટ્રૅક @@ -10,7 +24,7 @@ કોઈ રિપીટ કરતા નહીં એક રિપીટ કરો બધાને રિપીટ કરો - શફલ કરો + શફલ કરો પૂર્ણસ્ક્રીન મોડ VR મોડ ડાઉનલોડ કરો diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index 8ba92054ff..0435e3eb84 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -1,4 +1,18 @@ + पिछला ट्रैक अगला ट्रैक @@ -10,7 +24,7 @@ किसी को न दोहराएं एक को दोहराएं सभी को दोहराएं - शफ़ल करें + शफ़ल करें फ़ुलस्क्रीन मोड VR मोड डाउनलोड करें diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index 4fa1942986..b36c9ff9e7 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -1,4 +1,18 @@ + Prethodni zapis Sljedeći zapis @@ -10,7 +24,7 @@ Bez ponavljanja Ponovi jedno Ponovi sve - Reproduciraj nasumično + Reproduciraj nasumično Prikaz na cijelom zaslonu VR način Preuzmi diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index baf77650e0..ad67165cf8 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -1,4 +1,18 @@ + Előző szám Következő szám @@ -10,7 +24,7 @@ Nincs ismétlés Egy szám ismétlése Összes szám ismétlése - Keverés + Keverés Teljes képernyős mód VR-mód Letöltés diff --git a/library/ui/src/main/res/values-hy/strings.xml b/library/ui/src/main/res/values-hy/strings.xml index 04a2aeb140..31f4db37d2 100644 --- a/library/ui/src/main/res/values-hy/strings.xml +++ b/library/ui/src/main/res/values-hy/strings.xml @@ -1,4 +1,18 @@ + Նախորդը Հաջորդը @@ -10,7 +24,7 @@ Չկրկնել Կրկնել մեկը Կրկնել բոլորը - Խառնել + Խառնել Լիաէկրան ռեժիմ VR ռեժիմ Ներբեռնել diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 7410576e81..d7bae9719d 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -1,4 +1,18 @@ + Lagu sebelumnya Lagu berikutnya @@ -10,7 +24,7 @@ Jangan ulangi Ulangi 1 Ulangi semua - Acak + Acak Mode layar penuh Mode VR Download diff --git a/library/ui/src/main/res/values-is/strings.xml b/library/ui/src/main/res/values-is/strings.xml index bdb27a6648..4c09db5251 100644 --- a/library/ui/src/main/res/values-is/strings.xml +++ b/library/ui/src/main/res/values-is/strings.xml @@ -1,4 +1,18 @@ + Fyrra lag Næsta lag @@ -10,7 +24,7 @@ Endurtaka ekkert Endurtaka eitt Endurtaka allt - Stokka + Stokka Allur skjárinn sýndarveruleikastilling Sækja diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index ffa05821e9..e10a62a11b 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -1,4 +1,18 @@ + Traccia precedente Traccia successiva @@ -10,7 +24,7 @@ Non ripetere nulla Ripeti uno Ripeti tutto - Riproduzione casuale + Riproduzione casuale Modalità a schermo intero Modalità VR Scarica diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index 695196c5be..8dd08278a3 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -1,4 +1,18 @@ + הרצועה הקודמת הרצועה הבאה @@ -10,7 +24,7 @@ אל תחזור על אף פריט חזור על פריט אחד חזור על הכול - ערבוב + ערבוב מצב מסך מלא מצב VR הורדה diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index b4158736a8..dc479596b9 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -1,4 +1,18 @@ + 前のトラック 次のトラック @@ -10,7 +24,7 @@ リピートなし 1 曲をリピート 全曲をリピート - シャッフル + シャッフル 全画面モード VR モード ダウンロード diff --git a/library/ui/src/main/res/values-ka/strings.xml b/library/ui/src/main/res/values-ka/strings.xml index 13ceaaf51f..7b9ecc7a3a 100644 --- a/library/ui/src/main/res/values-ka/strings.xml +++ b/library/ui/src/main/res/values-ka/strings.xml @@ -1,4 +1,18 @@ + წინა ჩანაწერი შემდეგი ჩანაწერი @@ -10,7 +24,7 @@ არცერთის გამეორება ერთის გამეორება ყველას გამეორება - არეულად დაკვრა + არეულად დაკვრა სრულეკრანიანი რეჟიმი VR რეჟიმი ჩამოტვირთვა diff --git a/library/ui/src/main/res/values-kk/strings.xml b/library/ui/src/main/res/values-kk/strings.xml index 92119d1fe5..ef2c1ab2b7 100644 --- a/library/ui/src/main/res/values-kk/strings.xml +++ b/library/ui/src/main/res/values-kk/strings.xml @@ -1,4 +1,18 @@ + Алдыңғы аудиотрек Келесі аудиотрек @@ -10,7 +24,7 @@ Ешқайсысын қайталамау Біреуін қайталау Барлығын қайталау - Араластыру + Араластыру Толық экран режимі VR режимі Жүктеп алу diff --git a/library/ui/src/main/res/values-km/strings.xml b/library/ui/src/main/res/values-km/strings.xml index 62728de026..3636a6e6d6 100644 --- a/library/ui/src/main/res/values-km/strings.xml +++ b/library/ui/src/main/res/values-km/strings.xml @@ -1,4 +1,18 @@ + សំនៀង​​មុន សំនៀង​បន្ទាប់ @@ -10,7 +24,7 @@ មិន​លេង​ឡើងវិញ លេង​ឡើង​វិញ​ម្ដង លេង​ឡើងវិញ​ទាំងអស់ - ច្របល់ + ច្របល់ មុខងារពេញ​អេក្រង់ មុខងារ VR ទាញយក diff --git a/library/ui/src/main/res/values-kn/strings.xml b/library/ui/src/main/res/values-kn/strings.xml index 6e6bfcb165..85df144fca 100644 --- a/library/ui/src/main/res/values-kn/strings.xml +++ b/library/ui/src/main/res/values-kn/strings.xml @@ -1,4 +1,18 @@ + ಹಿಂದಿನ ಟ್ರ್ಯಾಕ್ ಮುಂದಿನ ಟ್ರ್ಯಾಕ್ @@ -10,7 +24,7 @@ ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ - ಶಫಲ್‌ + ಶಫಲ್‌ ಪೂರ್ಣ ಪರದೆ ಮೋಡ್ VR ಮೋಡ್ ಡೌನ್‌ಲೋಡ್‌ diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 230660ad6a..3442318047 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -1,4 +1,18 @@ + 이전 트랙 다음 트랙 @@ -10,7 +24,7 @@ 반복 안함 현재 미디어 반복 모두 반복 - 셔플 + 셔플 전체화면 모드 가상 현실 모드 다운로드 diff --git a/library/ui/src/main/res/values-ky/strings.xml b/library/ui/src/main/res/values-ky/strings.xml index 57b8bbb5f5..a4c5b36a6c 100644 --- a/library/ui/src/main/res/values-ky/strings.xml +++ b/library/ui/src/main/res/values-ky/strings.xml @@ -1,4 +1,18 @@ + Мурунку трек Кийинки трек @@ -10,7 +24,7 @@ Кайталанбасын Бирөөнү кайталоо Баарын кайталоо - Аралаштыруу + Аралаштыруу Толук экран режими VR режими Жүктөп алуу diff --git a/library/ui/src/main/res/values-lo/strings.xml b/library/ui/src/main/res/values-lo/strings.xml index d7996610b2..8d380f2808 100644 --- a/library/ui/src/main/res/values-lo/strings.xml +++ b/library/ui/src/main/res/values-lo/strings.xml @@ -1,4 +1,18 @@ + ເພງກ່ອນໜ້າ ເພງຕໍ່ໄປ @@ -10,7 +24,7 @@ ບໍ່ຫຼິ້ນຊ້ຳ ຫຼິ້ນຊໍ້າ ຫຼິ້ນຊ້ຳທັງໝົດ - ຫຼີ້ນແບບສຸ່ມ + ຫຼີ້ນແບບສຸ່ມ ໂໝດເຕັມຈໍ ໂໝດ VR ດາວໂຫລດ diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index 3e9a63dc99..1b3cfe4573 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -1,4 +1,18 @@ + Ankstesnis takelis Kitas takelis @@ -10,7 +24,7 @@ Nekartoti nieko Kartoti vieną Kartoti viską - Maišyti + Maišyti Viso ekrano režimas VR režimas Atsisiųsti diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index 59b541808a..6d7a232bcc 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -1,4 +1,18 @@ + Iepriekšējais ieraksts Nākamais ieraksts @@ -10,7 +24,7 @@ Neatkārtot nevienu Atkārtot vienu Atkārtot visu - Atskaņot jauktā secībā + Atskaņot jauktā secībā Pilnekrāna režīms VR režīms Lejupielādēt diff --git a/library/ui/src/main/res/values-mk/strings.xml b/library/ui/src/main/res/values-mk/strings.xml index 08a54d7240..1ad12a14d7 100644 --- a/library/ui/src/main/res/values-mk/strings.xml +++ b/library/ui/src/main/res/values-mk/strings.xml @@ -1,4 +1,18 @@ + Претходна песна Следна песна @@ -10,7 +24,7 @@ Не повторувај ниту една Повтори една Повтори ги сите - Измешај + Измешај Режим на цел екран Режим на VR Преземи diff --git a/library/ui/src/main/res/values-ml/strings.xml b/library/ui/src/main/res/values-ml/strings.xml index 6e79db0903..a227434e7a 100644 --- a/library/ui/src/main/res/values-ml/strings.xml +++ b/library/ui/src/main/res/values-ml/strings.xml @@ -1,4 +1,18 @@ + മുമ്പത്തെ ട്രാക്ക് അടുത്ത ട്രാക്ക് @@ -10,7 +24,7 @@ ഒന്നും ആവർത്തിക്കരുത് ഒരെണ്ണം ആവർത്തിക്കുക എല്ലാം ആവർത്തിക്കുക - ഇടകലര്‍ത്തുക + ഇടകലര്‍ത്തുക പൂർണ്ണ സ്‌ക്രീൻ മോഡ് VR മോഡ് ഡൗൺലോഡ് diff --git a/library/ui/src/main/res/values-mn/strings.xml b/library/ui/src/main/res/values-mn/strings.xml index 383d102520..8b8df3f9d4 100644 --- a/library/ui/src/main/res/values-mn/strings.xml +++ b/library/ui/src/main/res/values-mn/strings.xml @@ -1,4 +1,18 @@ + Өмнөх бичлэг Дараагийн бичлэг @@ -10,7 +24,7 @@ Алийг нь ч дахин тоглуулахгүй Одоогийн тоглуулж буй медиаг дахин тоглуулах Бүгдийг нь дахин тоглуулах - Холих + Холих Бүтэн дэлгэцийн горим VR горим Татах diff --git a/library/ui/src/main/res/values-mr/strings.xml b/library/ui/src/main/res/values-mr/strings.xml index a0900ab851..5c2bbc738c 100644 --- a/library/ui/src/main/res/values-mr/strings.xml +++ b/library/ui/src/main/res/values-mr/strings.xml @@ -1,4 +1,18 @@ + मागील ट्रॅक पुढील ट्रॅक @@ -10,7 +24,7 @@ रीपीट करू नका एक रीपीट करा सर्व रीपीट करा - शफल करा + शफल करा फुल स्क्रीन मोड VR मोड डाउनलोड करा diff --git a/library/ui/src/main/res/values-ms/strings.xml b/library/ui/src/main/res/values-ms/strings.xml index 6dab5be8de..8bc50c7605 100644 --- a/library/ui/src/main/res/values-ms/strings.xml +++ b/library/ui/src/main/res/values-ms/strings.xml @@ -1,4 +1,18 @@ + Lagu sebelumnya Lagu seterusnya @@ -10,7 +24,7 @@ Jangan ulang Ulang satu Ulang semua - Rombak + Rombak Mod skrin penuh Mod VR Muat turun diff --git a/library/ui/src/main/res/values-my/strings.xml b/library/ui/src/main/res/values-my/strings.xml index b30b76d516..e8a88a312d 100644 --- a/library/ui/src/main/res/values-my/strings.xml +++ b/library/ui/src/main/res/values-my/strings.xml @@ -1,4 +1,18 @@ + ယခင် တစ်ပုဒ် နောက် တစ်ပုဒ် @@ -10,7 +24,7 @@ မည်သည်ကိုမျှ ပြန်မကျော့ရန် တစ်ခုကို ပြန်ကျော့ရန် အားလုံး ပြန်ကျော့ရန် - ရောသမမွှေ + ရောသမမွှေ မျက်နှာပြင်အပြည့် မုဒ် VR မုဒ် ဒေါင်းလုဒ် လုပ်ရန် diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index f2847dd829..f9a0850bec 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -1,4 +1,18 @@ + Forrige spor Neste spor @@ -10,7 +24,7 @@ Ikke gjenta noen Gjenta én Gjenta alle - Tilfeldig rekkefølge + Tilfeldig rekkefølge Fullskjermmodus VR-modus Last ned diff --git a/library/ui/src/main/res/values-ne/strings.xml b/library/ui/src/main/res/values-ne/strings.xml index ff56480df1..f633a13af4 100644 --- a/library/ui/src/main/res/values-ne/strings.xml +++ b/library/ui/src/main/res/values-ne/strings.xml @@ -1,4 +1,18 @@ + अघिल्लो ट्रयाक अर्को ट्र्याक @@ -10,7 +24,7 @@ कुनै पनि नदोहोर्‍याउनुहोस् एउटा दोहोर्‍याउनुहोस् सबै दोहोर्‍याउनुहोस् - मिसाउनुहोस् + मिसाउनुहोस् पूर्ण स्क्रिन मोड VR मोड डाउनलोड गर्नुहोस् diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index 3fbf113f1e..4c71815136 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -1,4 +1,18 @@ + Vorige track Volgende track @@ -10,7 +24,7 @@ Niets herhalen Eén herhalen Alles herhalen - Shuffle + Shuffle Modus \'Volledig scherm\' VR-modus Downloaden diff --git a/library/ui/src/main/res/values-pa/strings.xml b/library/ui/src/main/res/values-pa/strings.xml index 9f25759878..0d30c2c519 100644 --- a/library/ui/src/main/res/values-pa/strings.xml +++ b/library/ui/src/main/res/values-pa/strings.xml @@ -1,4 +1,18 @@ + ਪਿਛਲਾ ਟਰੈਕ ਅਗਲਾ ਟਰੈਕ @@ -10,7 +24,7 @@ ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ - ਬੇਤਰਤੀਬ ਕਰੋ + ਬੇਤਰਤੀਬ ਕਰੋ ਪੂਰੀ-ਸਕ੍ਰੀਨ ਮੋਡ VR ਮੋਡ ਡਾਊਨਲੋਡ ਕਰੋ diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 8df3b62b0c..46f76e975a 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -1,4 +1,18 @@ + Poprzedni utwór Następny utwór @@ -10,7 +24,7 @@ Nie powtarzaj Powtórz jeden Powtórz wszystkie - Odtwarzanie losowe + Odtwarzanie losowe Tryb pełnoekranowy Tryb VR Pobierz diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index 188e18f6b5..60df32be81 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -1,4 +1,18 @@ + Faixa anterior Faixa seguinte @@ -10,7 +24,7 @@ Não repetir nenhum Repetir um Repetir tudo - Reproduzir aleatoriamente + Reproduzir aleatoriamente Modo de ecrã inteiro Modo de RV Transferir diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 9e83387a76..63f3abd343 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -1,4 +1,18 @@ + Faixa anterior Próxima faixa @@ -10,7 +24,7 @@ Não repetir Repetir uma Repetir tudo - Aleatório + Aleatório Modo de tela cheia Modo RV Fazer o download diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 9bb8cfc8ee..b7f5a6b63e 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -1,4 +1,18 @@ + Melodia anterioară Următoarea înregistrare @@ -10,7 +24,7 @@ Nu repetați niciunul Repetați unul Repetați-le pe toate - Redați aleatoriu + Redați aleatoriu Modul Ecran complet Mod RV Descărcați diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index e66a282da4..c72ea716bf 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -1,4 +1,18 @@ + Предыдущий трек Следующий трек @@ -10,7 +24,7 @@ Не повторять Повторять трек Повторять все - Перемешать + Перемешать Полноэкранный режим VR-режим Скачать diff --git a/library/ui/src/main/res/values-si/strings.xml b/library/ui/src/main/res/values-si/strings.xml index b6bfb1848f..19d37854fd 100644 --- a/library/ui/src/main/res/values-si/strings.xml +++ b/library/ui/src/main/res/values-si/strings.xml @@ -1,4 +1,18 @@ + පෙර ඛණ්ඩය ඊළඟ ඛණ්ඩය @@ -10,7 +24,7 @@ කිසිවක් පුනරාවර්තනය නොකරන්න එකක් පුනරාවර්තනය කරන්න සියල්ල පුනරාවර්තනය කරන්න - කලවම් කරන්න + කලවම් කරන්න සම්පූර්ණ තිර ප්‍රකාරය VR ප්‍රකාරය බාගන්න diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index 6d5ddaea28..c45fd13dcf 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -1,4 +1,18 @@ + Predchádzajúca skladba Ďalšia skladba @@ -10,7 +24,7 @@ Neopakovať Opakovať jednu Opakovať všetko - Náhodne prehrávať + Náhodne prehrávať Režim celej obrazovky režim VR Stiahnuť diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index 1e3adff704..17f1e66764 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -1,4 +1,18 @@ + Prejšnja skladba Naslednja skladba @@ -10,7 +24,7 @@ Brez ponavljanja Ponavljanje ene Ponavljanje vseh - Naključno predvajanje + Naključno predvajanje Celozaslonski način Način VR Prenos diff --git a/library/ui/src/main/res/values-sq/strings.xml b/library/ui/src/main/res/values-sq/strings.xml index d5b8903ed7..950c867e8b 100644 --- a/library/ui/src/main/res/values-sq/strings.xml +++ b/library/ui/src/main/res/values-sq/strings.xml @@ -1,4 +1,18 @@ + Kënga e mëparshme Kënga tjetër @@ -10,7 +24,7 @@ Mos përsërit asnjë Përsërit një Përsërit të gjitha - Përziej + Përziej Modaliteti me ekran të plotë Modaliteti RV Shkarko diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index b45fd8ab03..6c3074bc41 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -1,4 +1,18 @@ + Претходна песма Следећа песма @@ -10,7 +24,7 @@ Не понављај ниједну Понови једну Понови све - Пусти насумично + Пусти насумично Режим целог екрана ВР режим Преузми diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index 7af95a4632..c7dafaf786 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -1,4 +1,18 @@ + Föregående spår Nästa spår @@ -10,7 +24,7 @@ Upprepa inga Upprepa en Upprepa alla - Blanda spår + Blanda spår Helskärmsläge VR-läge Ladda ned diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index 1cdd325278..66568a3acc 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -1,4 +1,18 @@ + Wimbo uliotangulia Wimbo unaofuata @@ -10,7 +24,7 @@ Usirudie yoyote Rudia moja Rudia zote - Changanya + Changanya Hali ya skrini nzima Hali ya Uhalisia Pepe Pakua diff --git a/library/ui/src/main/res/values-ta/strings.xml b/library/ui/src/main/res/values-ta/strings.xml index 2b2b9e13d6..a4544299c0 100644 --- a/library/ui/src/main/res/values-ta/strings.xml +++ b/library/ui/src/main/res/values-ta/strings.xml @@ -1,4 +1,18 @@ + முந்தைய டிராக் அடுத்த டிராக் @@ -10,7 +24,7 @@ எதையும் மீண்டும் இயக்காதே இதை மட்டும் மீண்டும் இயக்கு அனைத்தையும் மீண்டும் இயக்கு - வரிசை மாற்றி இயக்கு + வரிசை மாற்றி இயக்கு முழுத்திரைப் பயன்முறை VR பயன்முறை பதிவிறக்கும் பட்டன் diff --git a/library/ui/src/main/res/values-te/strings.xml b/library/ui/src/main/res/values-te/strings.xml index ea344b0345..8fcb29cc2f 100644 --- a/library/ui/src/main/res/values-te/strings.xml +++ b/library/ui/src/main/res/values-te/strings.xml @@ -1,4 +1,18 @@ + మునుపటి ట్రాక్ తదుపరి ట్రాక్ @@ -10,7 +24,7 @@ దేన్నీ పునరావృతం చేయకండి ఒకదాన్ని పునరావృతం చేయండి అన్నింటినీ పునరావృతం చేయండి - షఫుల్ చేయండి + షఫుల్ చేయండి పూర్తి స్క్రీన్ మోడ్ వర్చువల్ రియాలిటీ మోడ్ డౌన్‌లోడ్ చేయి diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index 3cd933ccf1..918b62f099 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -1,4 +1,18 @@ + แทร็กก่อนหน้า แทร็กถัดไป @@ -10,7 +24,7 @@ ไม่เล่นซ้ำ เล่นซ้ำเพลงเดียว เล่นซ้ำทั้งหมด - สุ่ม + สุ่ม โหมดเต็มหน้าจอ โหมด VR ดาวน์โหลด diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 21852c5011..df00a07299 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -1,4 +1,18 @@ + Nakaraang track Susunod na track @@ -10,7 +24,7 @@ Walang uulitin Mag-ulit ng isa Ulitin lahat - I-shuffle + I-shuffle Fullscreen mode VR mode I-download diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index 2fbf36514f..5005f0bfb9 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -1,4 +1,18 @@ + Önceki parça Sonraki parça @@ -10,7 +24,7 @@ Hiçbirini tekrarlama Birini tekrarla Tümünü tekrarla - Karıştır + Karıştır Tam ekran modu VR modu İndir diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 5d338b61af..a42a8128b3 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -1,4 +1,18 @@ + Попередня композиція Наступна композиція @@ -10,7 +24,7 @@ Не повторювати Повторити 1 Повторити всі - Перемішати + Перемішати Повноекранний режим Режим віртуальної реальності Завантажити diff --git a/library/ui/src/main/res/values-ur/strings.xml b/library/ui/src/main/res/values-ur/strings.xml index aa98b0728e..47f35d1bae 100644 --- a/library/ui/src/main/res/values-ur/strings.xml +++ b/library/ui/src/main/res/values-ur/strings.xml @@ -1,4 +1,18 @@ + پچھلا ٹریک اگلا ٹریک @@ -10,7 +24,7 @@ کسی کو نہ دہرائیں ایک کو دہرائیں سبھی کو دہرائیں - شفل کریں + شفل کریں پوری اسکرین والی وضع VR موڈ ڈاؤن لوڈ کریں diff --git a/library/ui/src/main/res/values-uz/strings.xml b/library/ui/src/main/res/values-uz/strings.xml index 2dcf5a518d..3d8e270636 100644 --- a/library/ui/src/main/res/values-uz/strings.xml +++ b/library/ui/src/main/res/values-uz/strings.xml @@ -1,4 +1,18 @@ + Avvalgi trek Keyingi trek @@ -10,7 +24,7 @@ Takrorlanmasin Bittasini takrorlash Hammasini takrorlash - Aralash + Aralash Butun ekran rejimi VR rejimi Yuklab olish diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 1cdb249ef0..dc78b504fd 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -1,4 +1,18 @@ + Bản nhạc trước Bản nhạc tiếp theo @@ -10,7 +24,7 @@ Không lặp lại Lặp lại một Lặp lại tất cả - Phát ngẫu nhiên + Phát ngẫu nhiên Chế độ toàn màn hình Chế độ thực tế ảo Tải xuống diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index fe21669ea4..d2c3fb93ca 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -1,4 +1,18 @@ + 上一曲 下一曲 @@ -10,7 +24,7 @@ 不重复播放 重复播放一项 全部重复播放 - 随机播放 + 随机播放 全屏模式 VR 模式 下载 diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index 56e0a1a53b..d040db1b03 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -1,4 +1,18 @@ + 上一首曲目 下一首曲目 @@ -10,7 +24,7 @@ 不重複播放 重複播放單一項目 全部重複播放 - 隨機播放 + 隨機播放 全螢幕模式 虛擬現實模式 下載 diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index 7b29f7924e..c3a1b5521e 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -1,4 +1,18 @@ + 上一首曲目 下一首曲目 @@ -10,7 +24,7 @@ 不重複播放 重複播放單一項目 重複播放所有項目 - 隨機播放 + 隨機播放 全螢幕模式 虛擬實境模式 下載 diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index 83cf9b2e97..08922a5037 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -1,4 +1,18 @@ + Ithrekhi yangaphambilini Ithrekhi elandelayo @@ -10,7 +24,7 @@ Phinda okungekho Phinda okukodwa Phinda konke - Shova + Shova Imodi yesikrini esigcwele Inqubo ye-VR Landa diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index bbb4aca8d5..e3f1c3aaec 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -34,8 +34,10 @@ Repeat one Repeat all - - Shuffle + + Shuffle on + + Shuffle off Fullscreen mode diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index e73524815a..c458a3ea99 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -51,11 +51,6 @@ @string/exo_controls_pause_description - - + + diff --git a/settings.gradle b/settings.gradle index 50fdb68f30..2708596a9e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,11 +22,13 @@ include modulePrefix + 'demo' include modulePrefix + 'demo-cast' include modulePrefix + 'demo-ima' include modulePrefix + 'demo-gvr' +include modulePrefix + 'demo-surface' 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 + 'demo-surface').projectDir = new File(rootDir, 'demos/surface') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' From c2d9960a6e3a6a790a11a3842e72f8cda5d6e946 Mon Sep 17 00:00:00 2001 From: Cai Yuanqing Date: Wed, 2 Oct 2019 13:25:26 +1300 Subject: [PATCH 456/807] Issue: #6501 Wrong segmentNumShift was calculated in copyWithNewRepresentation In DefaultDashChunkSource.copyWithNewRepresentation, it will handle the logic that new MPD manifest file is updated and calculate a newSegmentNumShift for furthermore segNum index calculation in getSegmentUrl, when a shorter window MPD updated and then back to a longer window MPD, copyWithNewRepresentation will go into the overlap case but the new index actually contains the old index.. --- .../android/exoplayer2/source/dash/DefaultDashChunkSource.java | 2 ++ 1 file changed, 2 insertions(+) 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 cd39c9538a..14fe81f605 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 @@ -700,6 +700,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. throw new BehindLiveWindowException(); + } else if (oldIndex.getFirstSegmentNum() >= newIndexFirstSegmentNum) { + // The new index contains the old one, continue process the next segment } else { // The new index overlaps with the old one. newSegmentNumShift += From 47ad497aa8d7dcf1a13e7a4f2d637814d5d1db46 Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Mon, 23 Sep 2019 11:25:46 +0100 Subject: [PATCH 457/807] Add android config option for vp9 build PiperOrigin-RevId: 270640701 --- extensions/vp9/src/main/METADATA | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 extensions/vp9/src/main/METADATA diff --git a/extensions/vp9/src/main/METADATA b/extensions/vp9/src/main/METADATA new file mode 100644 index 0000000000..7510fdadc0 --- /dev/null +++ b/extensions/vp9/src/main/METADATA @@ -0,0 +1,9 @@ +# Format: google3/devtools/metadata/metadata.proto (go/google3metadata) + +tricorder: { + options: { + builder: { + config: "android" + } + } +} From 5695bae9d8fde5e156fd38fa552e266c5611c71f Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 23 Sep 2019 14:27:35 +0100 Subject: [PATCH 458/807] Remove DataSpec.FLAG_ALLOW_ICY_METADATA Remove the flag DataSpec.FLAG_ALLOW_ICY_METADATA. Instead, set the header IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME in the DataSpec httpRequestHeaders. BUG:134034248 PiperOrigin-RevId: 270662676 --- RELEASENOTES.md | 3 + .../ext/cronet/CronetDataSource.java | 10 +- .../ext/okhttp/OkHttpDataSource.java | 7 +- .../source/ProgressiveMediaPeriod.java | 18 +++- .../android/exoplayer2/upstream/DataSpec.java | 98 ++++++++++++++--- .../upstream/DefaultHttpDataSource.java | 12 +-- .../exoplayer2/upstream/DataSpecTest.java | 100 ++++++++++++++---- 7 files changed, 186 insertions(+), 62 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eb983e7524..11b493d555 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### dev-v2 (not yet released) ### +* Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Instead, set the header + `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` in the `DataSpec` + `httpRequestHeaders`. * DASH: Support negative @r values in segment timelines ([#1787](https://github.com/google/ExoPlayer/issues/1787)). * Add `allowedCapturePolicy` field to `AudioAttributes` wrapper to allow to 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 91bceb9aee..1903e33995 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 @@ -22,7 +22,6 @@ import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -706,11 +705,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) { throw new IOException("HTTP request with non-empty body must set Content-Type"); } - if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) { - requestBuilder.addHeader( - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); - } + // Set the Range header. if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) { StringBuilder rangeValue = new StringBuilder(); @@ -937,7 +932,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { dataSpec.position, dataSpec.length, dataSpec.key, - dataSpec.flags); + dataSpec.flags, + dataSpec.httpRequestHeaders); } else { redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl)); } 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 2a8f671577..3053961f49 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -21,7 +21,6 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -361,11 +360,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { builder.addHeader("Accept-Encoding", "identity"); } - if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) { - builder.addHeader( - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); - } + RequestBody requestBody = null; if (dataSpec.httpBody != null) { requestBody = RequestBody.create(null, dataSpec.httpBody); diff --git a/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 7cb767854f..c768fe4981 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 @@ -55,6 +55,9 @@ import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.checkerframework.checker.nullness.compatqual.NullableType; /** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ @@ -86,6 +89,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000; + private static final Map ICY_METADATA_HEADERS = createIcyMetadataHeaders(); + private static final Format ICY_FORMAT = Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE); @@ -1025,9 +1030,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; position, C.LENGTH_UNSET, customCacheKey, - DataSpec.FLAG_ALLOW_ICY_METADATA - | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN - | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); + DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION, + ICY_METADATA_HEADERS); } private void setLoadPosition(long position, long timeUs) { @@ -1154,4 +1158,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return 31 * id + (isIcyTrack ? 1 : 0); } } + + private static Map createIcyMetadataHeaders() { + Map headers = new HashMap<>(); + headers.put( + IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, + IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); + return Collections.unmodifiableMap(headers); + } } 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 f5802bed4e..acf5550427 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 @@ -35,19 +35,14 @@ public final class DataSpec { /** * The flags that apply to any request for data. Possible flag values are {@link - * #FLAG_ALLOW_GZIP}, {@link #FLAG_ALLOW_ICY_METADATA}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} - * and {@link #FLAG_ALLOW_CACHE_FRAGMENTATION}. + * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} and {@link + * #FLAG_ALLOW_CACHE_FRAGMENTATION}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = { - FLAG_ALLOW_GZIP, - FLAG_ALLOW_ICY_METADATA, - FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, - FLAG_ALLOW_CACHE_FRAGMENTATION - }) + value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION}) public @interface Flags {} /** * Allows an underlying network stack to request that the server use gzip compression. @@ -61,17 +56,15 @@ public final class DataSpec { * DataSource#read(byte[], int, int)} will be the decompressed data. */ public static final int FLAG_ALLOW_GZIP = 1; - /** Allows an underlying network stack to request that the stream contain ICY metadata. */ - public static final int FLAG_ALLOW_ICY_METADATA = 1 << 1; // 2 /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ - public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 2; // 4 + public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2 /** * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy * will be able to evict individual fragments of the data. Depending on the cache implementation, * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment * whilst writing another). */ - public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 3; // 8 + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; // 4 /** * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link @@ -172,6 +165,36 @@ public final class DataSpec { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); } + /** + * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition} and has + * request headers. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders} + */ + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { + this( + uri, + inferHttpMethod(null), + null, + absoluteStreamPosition, + absoluteStreamPosition, + length, + key, + flags, + httpRequestHeaders); + } + /** * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}. * @@ -216,7 +239,7 @@ public final class DataSpec { @Flags int flags) { this( uri, - /* httpMethod= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET, + /* httpMethod= */ inferHttpMethod(postBody), /* httpBody= */ postBody, absoluteStreamPosition, position, @@ -403,4 +426,53 @@ public final class DataSpec { flags, httpRequestHeaders); } + + /** + * Returns a copy of this data spec with the specified request headers. + * + * @param requestHeaders The HTTP request headers. + * @return The copied data spec with the specified request headers. + */ + public DataSpec withRequestHeaders(Map requestHeaders) { + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + requestHeaders); + } + + /** + * Returns a copy this data spec with additional request headers. + * + *

        Note: Values in {@code requestHeaders} will overwrite values with the same header key that + * were previously set in this instance's {@code #httpRequestHeaders}. + * + * @param requestHeaders The additional HTTP request headers. + * @return The copied data with the additional HTTP request headers. + */ + public DataSpec withAdditionalHeaders(Map requestHeaders) { + Map totalHeaders = new HashMap<>(this.httpRequestHeaders); + totalHeaders.putAll(requestHeaders); + + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + totalHeaders); + } + + @HttpMethod + private static int inferHttpMethod(@Nullable byte[] postBody) { + return postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET; + } } 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 37329a4868..436cad0d64 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 @@ -20,7 +20,6 @@ import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -432,7 +431,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); - boolean allowIcyMetadata = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA); if (!allowCrossProtocolRedirects) { // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection @@ -444,7 +442,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou position, length, allowGzip, - allowIcyMetadata, /* followRedirects= */ true, dataSpec.httpRequestHeaders); } @@ -460,7 +457,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou position, length, allowGzip, - allowIcyMetadata, /* followRedirects= */ false, dataSpec.httpRequestHeaders); int responseCode = connection.getResponseCode(); @@ -502,7 +498,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param position The byte offset of the requested data. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param allowGzip Whether to allow the use of gzip. - * @param allowIcyMetadata Whether to allow ICY metadata. * @param followRedirects Whether to follow redirects. * @param requestParameters parameters (HTTP headers) to include in request. */ @@ -513,7 +508,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou long position, long length, boolean allowGzip, - boolean allowIcyMetadata, boolean followRedirects, Map requestParameters) throws IOException { @@ -541,14 +535,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } connection.setRequestProperty("User-Agent", userAgent); connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity"); - if (allowIcyMetadata) { - connection.setRequestProperty( - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); - } connection.setInstanceFollowRedirects(followRedirects); connection.setDoOutput(httpBody != null); connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); + if (httpBody != null) { connection.setFixedLengthStreamingMode(httpBody.length); connection.connect(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java index f6e30f814a..2323dfe965 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.java @@ -98,31 +98,87 @@ public class DataSpecTest { } @Test - public void copyMethods_copiesHttpRequestHeaders() { - Map httpRequestParameters = new HashMap<>(); - httpRequestParameters.put("key1", "value1"); - httpRequestParameters.put("key2", "value2"); - httpRequestParameters.put("key3", "value3"); - - DataSpec dataSpec = - new DataSpec( - Uri.parse("www.google.com"), - /* httpMethod= */ 0, - /* httpBody= */ new byte[] {0, 0, 0, 0}, - /* absoluteStreamPosition= */ 0, - /* position= */ 0, - /* length= */ 1, - /* key= */ "key", - /* flags= */ 0, - httpRequestParameters); + public void withUri_copiesHttpRequestHeaders() { + Map httpRequestProperties = createRequestProperties(5); + DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse("www.new-uri.com")); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); - dataSpecCopy = dataSpec.subrange(2); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestProperties); + } - dataSpecCopy = dataSpec.subrange(2, 2); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters); + @Test + public void subrange_copiesHttpRequestHeaders() { + Map httpRequestProperties = createRequestProperties(5); + DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); + + DataSpec dataSpecCopy = dataSpec.subrange(2); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestProperties); + } + + @Test + public void subrange_withOffsetAndLength_copiesHttpRequestHeaders() { + Map httpRequestProperties = createRequestProperties(5); + DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); + + DataSpec dataSpecCopy = dataSpec.subrange(2, 2); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestProperties); + } + + @Test + public void withRequestHeaders_setsCorrectHeaders() { + Map httpRequestProperties = createRequestProperties(5); + DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); + + Map newRequestHeaders = createRequestProperties(5, 10); + DataSpec dataSpecCopy = dataSpec.withRequestHeaders(newRequestHeaders); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(newRequestHeaders); + } + + @Test + public void withAdditionalHeaders_setsCorrectHeaders() { + Map httpRequestProperties = createRequestProperties(5); + DataSpec dataSpec = createDataSpecWithHeaders(httpRequestProperties); + Map additionalHeaders = createRequestProperties(5, 10); + // additionalHeaders may overwrite a header key + String existingKey = httpRequestProperties.keySet().iterator().next(); + additionalHeaders.put(existingKey, "overwritten"); + Map expectedHeaders = new HashMap<>(httpRequestProperties); + expectedHeaders.putAll(additionalHeaders); + + DataSpec dataSpecCopy = dataSpec.withAdditionalHeaders(additionalHeaders); + + assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(expectedHeaders); + } + + private static Map createRequestProperties(int howMany) { + return createRequestProperties(0, howMany); + } + + private static Map createRequestProperties(int from, int to) { + assertThat(from).isLessThan(to); + + Map httpRequestParameters = new HashMap<>(); + for (int i = from; i < to; i++) { + httpRequestParameters.put("key-" + i, "value-" + i); + } + + return httpRequestParameters; + } + + private static DataSpec createDataSpecWithHeaders(Map httpRequestProperties) { + return new DataSpec( + Uri.parse("www.google.com"), + /* httpMethod= */ 0, + /* httpBody= */ new byte[] {0, 0, 0, 0}, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ 1, + /* key= */ "key", + /* flags= */ 0, + httpRequestProperties); } } From c8ea831a1e045aef0db28c0248ef55153410ca69 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 23 Sep 2019 15:15:41 +0100 Subject: [PATCH 459/807] Change default bandwidth fraction in AdaptiveTrackSelection. A reduced fraction of 0.7 was shown to better balance the rebuffer/quality trade-off. PiperOrigin-RevId: 270670465 --- .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fc3783d56b..eae21b5b30 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 @@ -285,7 +285,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; - public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f; public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; From 198366784574d422362e8c3e6521c9b0360e3b60 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 23 Sep 2019 15:18:25 +0100 Subject: [PATCH 460/807] Add a `cd` command to ExoPlayer clone instructions With this missing, the `checkout` command errors with: $ git checkout release-v2 fatal: not a git repository (or any of the parent directories): .git PiperOrigin-RevId: 270670796 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a369b077f4..d488f4113e 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ branch: ```sh git clone https://github.com/google/ExoPlayer.git +cd ExoPlayer git checkout release-v2 ``` From cc8b774b41fbfadb99421af1fd6922042c6c82bc Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 23 Sep 2019 16:28:22 +0100 Subject: [PATCH 461/807] Remove DefaultDrmSessionManager factory methods that enforce MediaDrm leaks Inline invocations of these methods, which still leaks the MediaDrms. However, it will be fixed once the DefaultDrmSessionManager API is finalized and ExoMediaDrm.Providers are introduced. Issue:#4721 PiperOrigin-RevId: 270681467 --- RELEASENOTES.md | 2 + .../drm/DefaultDrmSessionManager.java | 60 ------------------- .../playbacktests/gts/DashTestRunner.java | 9 ++- 3 files changed, 10 insertions(+), 61 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 11b493d555..664a22e15e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ `httpRequestHeaders`. * DASH: Support negative @r values in segment timelines ([#1787](https://github.com/google/ExoPlayer/issues/1787)). +* Remove `DefaultDrmSessionManager` factory methods that leak `ExoMediaDrm` + instances ([#4721](https://github.com/google/ExoPlayer/issues/4721)). * Add `allowedCapturePolicy` field to `AudioAttributes` wrapper to allow to opt-out of audio recording. * Add `DataSpec.httpRequestHeaders` to set HTTP request headers when connecting 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 759f88cdab..6c90a3660d 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 @@ -20,7 +20,6 @@ import android.annotation.TargetApi; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -106,65 +105,6 @@ public class DefaultDrmSessionManager /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler; - /** - * Instantiates a new instance using the Widevine scheme. - * - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newWidevineInstance( - MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters) - throws UnsupportedDrmException { - return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters); - } - - /** - * Instantiates a new instance using the PlayReady scheme. - * - *

        Note that PlayReady is unsupported by most Android devices, with the exception of Android TV - * devices, which do provide support. - * - * @param callback Performs key and provisioning requests. - * @param customData Optional custom data to include in requests generated by the instance. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newPlayReadyInstance( - MediaDrmCallback callback, @Nullable String customData) throws UnsupportedDrmException { - HashMap optionalKeyRequestParameters; - if (!TextUtils.isEmpty(customData)) { - optionalKeyRequestParameters = new HashMap<>(); - optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData); - } else { - optionalKeyRequestParameters = null; - } - return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters); - } - - /** - * Instantiates a new instance. - * - * @param uuid The UUID of the drm scheme. - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newFrameworkInstance( - UUID uuid, - MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters) - throws UnsupportedDrmException { - return new DefaultDrmSessionManager<>( - uuid, - FrameworkMediaDrm.newInstance(uuid), - callback, - optionalKeyRequestParameters, - /* multiSession= */ false, - INITIAL_DRM_REQUEST_RETRY_COUNT); - } - /** * @param uuid The UUID of the drm scheme. * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. 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 0d966c9080..fb64a7d13b 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 @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; 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; import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; @@ -266,7 +267,13 @@ public final class DashTestRunner { MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory(userAgent)); DefaultDrmSessionManager drmSessionManager = - DefaultDrmSessionManager.newWidevineInstance(drmCallback, null); + new DefaultDrmSessionManager<>( + C.WIDEVINE_UUID, + FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + drmCallback, + /* optionalKeyRequestParameters= */ null, + /* multiSession= */ false, + DefaultDrmSessionManager.INITIAL_DRM_REQUEST_RETRY_COUNT); if (!useL1Widevine) { drmSessionManager.setPropertyString( SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); From 4ad4e3e4fcf2480bc36e7034026a3538ec7664be Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 23 Sep 2019 19:59:09 +0100 Subject: [PATCH 462/807] Rollback of https://github.com/google/ExoPlayer/commit/3b22db33ba944df6829b1eff328efb0cd25e1678 *** Original commit *** add top-level playlist API to ExoPlayer Public design doc: https://docs.google.com/document/d/11h0S91KI5TB3NNZUtsCzg0S7r6nyTnF_tDZZAtmY93g Issue: #6161 *** PiperOrigin-RevId: 270728267 --- .../exoplayer2/demo/PlayerActivity.java | 52 +- .../exoplayer2/ext/cast/CastPlayer.java | 14 +- .../exoplayer2/ext/ima/FakePlayer.java | 15 +- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 4 +- .../google/android/exoplayer2/ExoPlayer.java | 158 +-- .../android/exoplayer2/ExoPlayerFactory.java | 10 +- .../android/exoplayer2/ExoPlayerImpl.java | 293 +---- .../exoplayer2/ExoPlayerImplInternal.java | 333 ++--- .../android/exoplayer2/MediaPeriodHolder.java | 24 +- .../android/exoplayer2/MediaPeriodQueue.java | 7 +- .../com/google/android/exoplayer2/Player.java | 25 +- .../google/android/exoplayer2/Playlist.java | 708 ----------- .../android/exoplayer2/SimpleExoPlayer.java | 179 +-- .../google/android/exoplayer2/Timeline.java | 69 - .../analytics/AnalyticsCollector.java | 17 +- .../AbstractConcatenatedTimeline.java | 50 +- .../source/ConcatenatingMediaSource.java | 1 - .../exoplayer2/source/LoopingMediaSource.java | 1 - .../exoplayer2/source/MaskingMediaSource.java | 12 +- .../android/exoplayer2/util/EventLogger.java | 10 +- .../google/android/exoplayer2/util/Util.java | 37 - .../android/exoplayer2/ExoPlayerTest.java | 1106 ++++------------- .../exoplayer2/MediaPeriodQueueTest.java | 111 +- .../android/exoplayer2/PlaylistTest.java | 510 -------- .../android/exoplayer2/TimelineTest.java | 140 --- .../analytics/AnalyticsCollectorTest.java | 101 +- .../android/exoplayer2/testutil/Action.java | 269 +--- .../exoplayer2/testutil/ActionSchedule.java | 129 +- .../exoplayer2/testutil/ExoHostedTest.java | 3 +- .../testutil/ExoPlayerTestRunner.java | 81 +- .../exoplayer2/testutil/StubExoPlayer.java | 83 -- .../android/exoplayer2/testutil/TestUtil.java | 58 - 32 files changed, 755 insertions(+), 3855 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/Playlist.java rename library/core/src/main/java/com/google/android/exoplayer2/{ => source}/AbstractConcatenatedTimeline.java (89%) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java 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 a9d1db64ad..347f49e27c 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 @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -78,7 +79,6 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.ArrayList; -import java.util.List; import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ @@ -141,7 +141,7 @@ public class PlayerActivity extends AppCompatActivity private DataSource.Factory dataSourceFactory; private SimpleExoPlayer player; - private List mediaSources; + private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; private DebugTextViewHelper debugViewHelper; @@ -343,8 +343,8 @@ public class PlayerActivity extends AppCompatActivity Intent intent = getIntent(); releaseMediaDrms(); - mediaSources = createTopLevelMediaSources(intent); - if (mediaSources.isEmpty()) { + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { return; } @@ -388,12 +388,12 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.setMediaItems(mediaSources, /* resetPosition= */ !haveStartPosition); - player.prepare(); + player.prepare(mediaSource, !haveStartPosition, false); updateButtonVisibility(); } - private List createTopLevelMediaSources(Intent intent) { + @Nullable + private MediaSource createTopLevelMediaSource(Intent intent) { String action = intent.getAction(); boolean actionIsListView = ACTION_VIEW_LIST.equals(action); if (!actionIsListView && !ACTION_VIEW.equals(action)) { @@ -421,30 +421,34 @@ public class PlayerActivity extends AppCompatActivity } } - List mediaSources = new ArrayList<>(); - for (UriSample sample : samples) { - mediaSources.add(createLeafMediaSource(sample)); + MediaSource[] mediaSources = new MediaSource[samples.length]; + for (int i = 0; i < samples.length; i++) { + mediaSources[i] = createLeafMediaSource(samples[i]); } - if (seenAdsTagUri && mediaSources.size() == 1) { + MediaSource mediaSource = + mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); + + if (seenAdsTagUri) { Uri adTagUri = samples[0].adTagUri; - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri); - if (adsMediaSource != null) { - mediaSources.set(0, adsMediaSource); + if (actionIsListView) { + showToast(R.string.unsupported_ads_in_concatenation); } else { - showToast(R.string.ima_not_loaded); + 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 if (seenAdsTagUri && mediaSources.size() > 1) { - showToast(R.string.unsupported_ads_in_concatenation); - releaseAdsLoader(); } else { releaseAdsLoader(); } - return mediaSources; + return mediaSource; } private MediaSource createLeafMediaSource(UriSample parameters) { @@ -544,7 +548,7 @@ public class PlayerActivity extends AppCompatActivity debugViewHelper = null; player.release(); player = null; - mediaSources = null; + mediaSource = null; trackSelector = null; } if (adsLoader != null) { 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 91a4c86cf2..2210f2c8b2 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 @@ -106,6 +106,7 @@ public final class CastPlayer extends BasePlayer { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; + private boolean waitingForInitialTimeline; /** * @param castContext The context from which the cast session is obtained. @@ -167,6 +168,7 @@ public final class CastPlayer extends BasePlayer { MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; + waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); } @@ -592,13 +594,15 @@ public final class CastPlayer extends BasePlayer { private void maybeUpdateTimelineAndNotify() { if (updateTimeline()) { - // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and - // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. + @Player.TimelineChangeReason + int reason = + waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; notificationsBatch.add( new ListenerNotificationTask( - listener -> - listener.onTimelineChanged( - currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE))); + listener -> listener.onTimelineChanged(currentTimeline, reason))); } } 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 a6a725ee9e..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 @@ -30,6 +30,7 @@ import java.util.ArrayList; private final Timeline.Period period; private final Timeline timeline; + private boolean prepared; @Player.State private int state; private boolean playWhenReady; private long position; @@ -46,17 +47,13 @@ import java.util.ArrayList; timeline = Timeline.EMPTY; } - /** - * Sets the timeline on this fake player, which notifies listeners with the changed timeline and - * the given timeline change reason. - * - * @param timeline The new timeline. - * @param timelineChangeReason The reason for the timeline change. - */ - public void updateTimeline(Timeline timeline, @TimelineChangeReason int timelineChangeReason) { + /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ + public void updateTimeline(Timeline timeline) { for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(timeline, timelineChangeReason); + listener.onTimelineChanged( + 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 a5c6b00619..2995df4ab4 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 @@ -285,9 +285,7 @@ public class ImaAdsLoaderTest { public void onAdPlaybackState(AdPlaybackState adPlaybackState) { adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); this.adPlaybackState = adPlaybackState; - fakeExoPlayer.updateTimeline( - new SinglePeriodAdTimeline(contentTimeline, adPlaybackState), - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); } @Override 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 4418549c8b..7c8a454191 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 @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -40,7 +39,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; -import java.util.List; /** * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link @@ -141,7 +139,7 @@ public interface ExoPlayer extends Player { private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private Looper looper; - @Nullable private AnalyticsCollector analyticsCollector; + private AnalyticsCollector analyticsCollector; private boolean useLazyPreparation; private boolean buildCalled; @@ -172,7 +170,7 @@ public interface ExoPlayer extends Player { new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), - /* analyticsCollector= */ null, + new AnalyticsCollector(Clock.DEFAULT), /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -199,7 +197,7 @@ public interface ExoPlayer extends Player { LoadControl loadControl, BandwidthMeter bandwidthMeter, Looper looper, - @Nullable AnalyticsCollector analyticsCollector, + AnalyticsCollector analyticsCollector, boolean useLazyPreparation, Clock clock) { Assertions.checkArgument(renderers.length > 0); @@ -320,156 +318,38 @@ public interface ExoPlayer extends Player { Assertions.checkState(!buildCalled); buildCalled = true; return new ExoPlayerImpl( - renderers, - trackSelector, - loadControl, - bandwidthMeter, - analyticsCollector, - useLazyPreparation, - clock, - looper); + renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); } } /** Returns the {@link Looper} associated with the playback thread. */ Looper getPlaybackLooper(); - /** @deprecated Use {@link #prepare()} instead. */ - @Deprecated + /** + * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback + * has not failed or been stopped. + */ void retry(); - /** @deprecated Use {@link #setMediaItem(MediaSource)} and {@link #prepare()} instead. */ - @Deprecated + /** + * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code + * prepare(mediaSource, true, true)}. + */ void prepare(MediaSource mediaSource); - /** @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #prepare()} instead. */ - @Deprecated - void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); - - /** Prepares the player. */ - void prepare(); - /** - * Clears the playlist and adds the specified {@link MediaSource MediaSources}. + * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback + * position the default position in the first {@link Timeline.Window}. * - * @param mediaItems The new {@link MediaSource MediaSources}. - */ - void setMediaItems(List mediaItems); - - /** - * Clears the playlist and adds the specified {@link MediaSource MediaSources}. - * - * @param mediaItems The new {@link MediaSource MediaSources}. + * @param mediaSource The {@link MediaSource} to play. * @param resetPosition Whether the playback position should be reset to the default position in * the first {@link Timeline.Window}. If false, playback will start from the position defined * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. + * @param resetState Whether the timeline, manifest, tracks and track selections should be reset. + * Should be true unless the player is being prepared to play the same media as it was playing + * previously (e.g. if playback failed and is being retried). */ - void setMediaItems(List mediaItems, boolean resetPosition); - - /** - * Clears the playlist and adds the specified {@link MediaSource MediaSources}. - * - * @param mediaItems The new {@link MediaSource MediaSources}. - * @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is - * passed, the current position is not reset. - * @param startPositionMs The position in milliseconds to start playback from. If {@link - * C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if - * {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the - * position is not reset at all. - */ - void setMediaItems(List mediaItems, int startWindowIndex, long startPositionMs); - - /** - * Clears the playlist and adds the specified {@link MediaSource}. - * - * @param mediaItem The new {@link MediaSource}. - */ - void setMediaItem(MediaSource mediaItem); - - /** - * Clears the playlist and adds the specified {@link MediaSource}. - * - * @param mediaItem The new {@link MediaSource}. - * @param startPositionMs The position in milliseconds to start playback from. - */ - void setMediaItem(MediaSource mediaItem, long startPositionMs); - - /** - * Adds a media item to the end of the playlist. - * - * @param mediaSource The {@link MediaSource} to add. - */ - void addMediaItem(MediaSource mediaSource); - - /** - * Adds a media item at the given index of the playlist. - * - * @param index The index at which to add the item. - * @param mediaSource The {@link MediaSource} to add. - */ - void addMediaItem(int index, MediaSource mediaSource); - - /** - * Adds a list of media items to the end of the playlist. - * - * @param mediaSources The {@link MediaSource MediaSources} to add. - */ - void addMediaItems(List mediaSources); - - /** - * Adds a list of media items at the given index of the playlist. - * - * @param index The index at which to add the media items. - * @param mediaSources The {@link MediaSource MediaSources} to add. - */ - void addMediaItems(int index, List mediaSources); - - /** - * Moves the media item at the current index to the new index. - * - * @param currentIndex The current index of the media item to move. - * @param newIndex The new index of the media item. If the new index is larger than the size of - * the playlist the item is moved to the end of the playlist. - */ - void moveMediaItem(int currentIndex, int newIndex); - - /** - * Moves the media item range to the new index. - * - * @param fromIndex The start of the range to move. - * @param toIndex The first item not to be included in the range (exclusive). - * @param newIndex The new index of the first media item of the range. If the new index is larger - * than the size of the remaining playlist after removing the range, the range is moved to the - * end of the playlist. - */ - void moveMediaItems(int fromIndex, int toIndex, int newIndex); - - /** - * Removes the media item at the given index of the playlist. - * - * @param index The index at which to remove the media item. - * @return The removed {@link MediaSource} or null if no item exists at the given index. - */ - @Nullable - MediaSource removeMediaItem(int index); - - /** - * Removes a range of media items from the playlist. - * - * @param fromIndex The index at which to start removing media items. - * @param toIndex The index of the first item to be kept (exclusive). - */ - void removeMediaItems(int fromIndex, int toIndex); - - /** Clears the playlist. */ - void clearMediaItems(); - - /** - * Sets the shuffle order. - * - * @param shuffleOrder The shuffle order. - */ - void setShuffleOrder(ShuffleOrder shuffleOrder); + void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message 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 b900491b1d..efe351c70a 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 @@ -296,7 +296,6 @@ public final class ExoPlayerFactory { drmSessionManager, bandwidthMeter, analyticsCollector, - /* useLazyPreparation= */ true, Clock.DEFAULT, looper); } @@ -345,13 +344,6 @@ public final class ExoPlayerFactory { BandwidthMeter bandwidthMeter, Looper looper) { return new ExoPlayerImpl( - renderers, - trackSelector, - loadControl, - bandwidthMeter, - /* analyticsCollector= */ null, - /* useLazyPreparation= */ true, - Clock.DEFAULT, - looper); + renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, 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 b9f29e2cb3..cbbf5cacbc 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 @@ -22,10 +22,8 @@ import android.os.Message; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -37,9 +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.Collections; -import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -66,20 +61,19 @@ import java.util.concurrent.CopyOnWriteArrayList; private final CopyOnWriteArrayList listeners; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; - private final List mediaSourceHolders; - private final boolean useLazyPreparation; + private MediaSource mediaSource; private boolean playWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; + private boolean hasPendingPrepare; private boolean hasPendingSeek; private boolean foregroundMode; private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; - private ShuffleOrder shuffleOrder; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -96,10 +90,6 @@ import java.util.concurrent.CopyOnWriteArrayList; * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollector The {@link AnalyticsCollector} that will be used by the instance. - * @param useLazyPreparation Whether playlist items are 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. * @param clock The {@link Clock} that will be used by the instance. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. @@ -110,8 +100,6 @@ import java.util.concurrent.CopyOnWriteArrayList; TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, - @Nullable AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" @@ -119,13 +107,10 @@ import java.util.concurrent.CopyOnWriteArrayList; Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); - this.useLazyPreparation = useLazyPreparation; - playWhenReady = false; - repeatMode = Player.REPEAT_MODE_OFF; - shuffleModeEnabled = false; - listeners = new CopyOnWriteArrayList<>(); - mediaSourceHolders = new ArrayList<>(); - shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); + this.playWhenReady = false; + this.repeatMode = Player.REPEAT_MODE_OFF; + this.shuffleModeEnabled = false; + this.listeners = new CopyOnWriteArrayList<>(); emptyTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[renderers.length], @@ -144,9 +129,6 @@ import java.util.concurrent.CopyOnWriteArrayList; }; playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); pendingListenerNotifications = new ArrayDeque<>(); - if (analyticsCollector != null) { - analyticsCollector.setPlayer(this); - } internalPlayer = new ExoPlayerImplInternal( renderers, @@ -157,7 +139,6 @@ import java.util.concurrent.CopyOnWriteArrayList; playWhenReady, repeatMode, shuffleModeEnabled, - analyticsCollector, eventHandler, clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); @@ -231,168 +212,41 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override - @Deprecated public void retry() { - prepare(); + if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } } @Override - public void prepare() { - if (playbackInfo.playbackState != Player.STATE_IDLE) { - return; - } + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + this.mediaSource = mediaSource; PlaybackInfo playbackInfo = getResetPlaybackInfo( - /* clearPlaylist= */ false, + resetPosition, + resetState, /* resetError= */ true, /* playbackState= */ Player.STATE_BUFFERING); // Trigger internal prepare first before updating the playback info and notifying external // listeners to ensure that new operations issued in the listener notifications reach the // player after this prepare. The internal player can't change the playback info immediately // because it uses a callback. + hasPendingPrepare = true; pendingOperationAcks++; - internalPlayer.prepare(); + internalPlayer.prepare(mediaSource, resetPosition, resetState); updatePlaybackInfo( playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + TIMELINE_CHANGE_REASON_RESET, /* seekProcessed= */ false); } - @Override - @Deprecated - public void prepare(MediaSource mediaSource) { - setMediaItem(mediaSource); - prepare(); - } - - @Override - @Deprecated - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - setMediaItem( - mediaSource, /* startPositionMs= */ resetPosition ? C.TIME_UNSET : getCurrentPosition()); - prepare(); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - setMediaItems(Collections.singletonList(mediaItem)); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs); - } - - @Override - public void setMediaItems(List mediaItems) { - setMediaItems( - mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs */ C.TIME_UNSET); - } - - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - setMediaItems( - mediaItems, - /* startWindowIndex= */ resetPosition ? C.INDEX_UNSET : getCurrentWindowIndex(), - /* startPositionMs= */ resetPosition ? C.TIME_UNSET : getCurrentPosition()); - } - - @Override - public void setMediaItems( - List mediaItems, int startWindowIndex, long startPositionMs) { - pendingOperationAcks++; - if (!mediaSourceHolders.isEmpty()) { - removeMediaSourceHolders( - /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); - } - List holders = addMediaSourceHolders(/* index= */ 0, mediaItems); - Timeline timeline = maskTimeline(); - internalPlayer.setMediaItems( - holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } - - @Override - public void addMediaItem(MediaSource mediaSource) { - addMediaItems(Collections.singletonList(mediaSource)); - } - - @Override - public void addMediaItem(int index, MediaSource mediaSource) { - addMediaItems(index, Collections.singletonList(mediaSource)); - } - - @Override - public void addMediaItems(List mediaSources) { - addMediaItems(/* index= */ mediaSourceHolders.size(), mediaSources); - } - - @Override - public void addMediaItems(int index, List mediaSources) { - Assertions.checkArgument(index >= 0); - pendingOperationAcks++; - List holders = addMediaSourceHolders(index, mediaSources); - Timeline timeline = maskTimeline(); - internalPlayer.addMediaItems(index, holders, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } - - @Override - public MediaSource removeMediaItem(int index) { - List mediaSourceHolders = - removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); - return mediaSourceHolders.isEmpty() ? null : mediaSourceHolders.get(0).mediaSource; - } - - @Override - public void removeMediaItems(int fromIndex, int toIndex) { - Assertions.checkArgument(toIndex > fromIndex); - removeMediaItemsInternal(fromIndex, toIndex); - } - - @Override - public void moveMediaItem(int currentIndex, int newIndex) { - Assertions.checkArgument(currentIndex != newIndex); - moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); - } - - @Override - public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { - Assertions.checkArgument( - fromIndex >= 0 - && fromIndex <= toIndex - && toIndex <= mediaSourceHolders.size() - && newFromIndex >= 0); - pendingOperationAcks++; - newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); - Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); - Timeline timeline = maskTimeline(); - internalPlayer.moveMediaItems(fromIndex, toIndex, newFromIndex, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } - - @Override - public void clearMediaItems() { - if (mediaSourceHolders.isEmpty()) { - return; - } - removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); - } - - @Override - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - pendingOperationAcks++; - this.shuffleOrder = shuffleOrder; - Timeline timeline = maskTimeline(); - internalPlayer.setShuffleOrder(shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - } @Override public void setPlayWhenReady(boolean playWhenReady) { @@ -550,9 +404,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void stop(boolean reset) { + if (reset) { + mediaSource = null; + } PlaybackInfo playbackInfo = getResetPlaybackInfo( - /* clearPlaylist= */ reset, + /* resetPosition= */ reset, + /* resetState= */ reset, /* resetError= */ reset, /* playbackState= */ Player.STATE_IDLE); // Trigger internal stop first before updating the playback info and notifying external @@ -565,7 +423,7 @@ import java.util.concurrent.CopyOnWriteArrayList; playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + TIMELINE_CHANGE_REASON_RESET, /* seekProcessed= */ false); } @@ -574,11 +432,13 @@ import java.util.concurrent.CopyOnWriteArrayList; Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + ExoPlayerLibraryInfo.registeredModules() + "]"); + mediaSource = null; internalPlayer.release(); eventHandler.removeCallbacksAndMessages(null); playbackInfo = getResetPlaybackInfo( - /* clearPlaylist= */ false, + /* resetPosition= */ false, + /* resetState= */ false, /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } @@ -726,11 +586,10 @@ import java.util.concurrent.CopyOnWriteArrayList; // 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) { case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: handlePlaybackInfo( - /* playbackInfo= */ (PlaybackInfo) msg.obj, + (PlaybackInfo) msg.obj, /* operationAcks= */ msg.arg1, /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, /* positionDiscontinuityReason= */ msg.arg2); @@ -778,23 +637,29 @@ import java.util.concurrent.CopyOnWriteArrayList; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } + @Player.TimelineChangeReason + int timelineChangeReason = + hasPendingPrepare + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; boolean seekProcessed = hasPendingSeek; + hasPendingPrepare = false; hasPendingSeek = false; updatePlaybackInfo( playbackInfo, positionDiscontinuity, positionDiscontinuityReason, - TIMELINE_CHANGE_REASON_SOURCE_UPDATE, + timelineChangeReason, seekProcessed); } } private PlaybackInfo getResetPlaybackInfo( - boolean clearPlaylist, boolean resetError, @Player.State int playbackState) { - if (clearPlaylist) { - // Reset list of media source holders which are used for creating the masking timeline. - removeMediaSourceHolders( - /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + boolean resetPosition, + boolean resetState, + boolean resetError, + @Player.State int playbackState) { + if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; maskingWindowPositionMs = 0; @@ -803,22 +668,24 @@ import java.util.concurrent.CopyOnWriteArrayList; maskingPeriodIndex = getCurrentPeriodIndex(); maskingWindowPositionMs = getCurrentPosition(); } + // Also reset period-based PlaybackInfo positions if resetting the state. + resetPosition = resetPosition || resetState; MediaPeriodId mediaPeriodId = - clearPlaylist + resetPosition ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) : playbackInfo.periodId; - long startPositionUs = clearPlaylist ? 0 : playbackInfo.positionUs; - long contentPositionUs = clearPlaylist ? C.TIME_UNSET : playbackInfo.contentPositionUs; + long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; + long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; return new PlaybackInfo( - clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? Timeline.EMPTY : playbackInfo.timeline, mediaPeriodId, startPositionUs, contentPositionUs, playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, startPositionUs, /* totalBufferedDurationUs= */ 0, @@ -828,8 +695,8 @@ import java.util.concurrent.CopyOnWriteArrayList; private void updatePlaybackInfo( PlaybackInfo playbackInfo, boolean positionDiscontinuity, - @DiscontinuityReason int positionDiscontinuityReason, - @TimelineChangeReason int timelineChangeReason, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, boolean seekProcessed) { boolean previousIsPlaying = isPlaying(); // Assign playback info immediately such that all getters return the right values. @@ -850,53 +717,6 @@ import java.util.concurrent.CopyOnWriteArrayList; /* isPlayingChanged= */ previousIsPlaying != isPlaying)); } - private List addMediaSourceHolders( - int index, List mediaSources) { - List holders = new ArrayList<>(); - for (int i = 0; i < mediaSources.size(); i++) { - Playlist.MediaSourceHolder holder = - new Playlist.MediaSourceHolder(mediaSources.get(i), useLazyPreparation); - holders.add(holder); - mediaSourceHolders.add(i + index, holder); - } - shuffleOrder = - shuffleOrder.cloneAndInsert( - /* insertionIndex= */ index, /* insertionCount= */ holders.size()); - return holders; - } - - private List removeMediaItemsInternal(int fromIndex, int toIndex) { - Assertions.checkArgument( - fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); - pendingOperationAcks++; - List mediaSourceHolders = - removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); - Timeline timeline = maskTimeline(); - internalPlayer.removeMediaItems(fromIndex, toIndex, shuffleOrder); - notifyListeners( - listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); - return mediaSourceHolders; - } - - private List removeMediaSourceHolders( - int fromIndex, int toIndexExclusive) { - List removed = new ArrayList<>(); - for (int i = toIndexExclusive - 1; i >= fromIndex; i--) { - removed.add(mediaSourceHolders.remove(i)); - } - shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); - return removed; - } - - private Timeline maskTimeline() { - playbackInfo = - playbackInfo.copyWithTimeline( - mediaSourceHolders.isEmpty() - ? Timeline.EMPTY - : new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); - return playbackInfo.timeline; - } - private void notifyListeners(ListenerInvocation listenerInvocation) { CopyOnWriteArrayList listenerSnapshot = new CopyOnWriteArrayList<>(listeners); notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation)); @@ -932,7 +752,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final TrackSelector trackSelector; private final boolean positionDiscontinuity; private final @Player.DiscontinuityReason int positionDiscontinuityReason; - private final int timelineChangeReason; + private final @Player.TimelineChangeReason int timelineChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; private final boolean playbackErrorChanged; @@ -966,16 +786,15 @@ import java.util.concurrent.CopyOnWriteArrayList; playbackErrorChanged = previousPlaybackInfo.playbackError != playbackInfo.playbackError && playbackInfo.playbackError != null; + timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; - timelineChanged = - !Util.areTimelinesSame(previousPlaybackInfo.timeline, playbackInfo.timeline); trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; } @Override public void run() { - if (timelineChanged) { + if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { invokeAll( listenerSnapshot, listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); 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 9cad08ab7b..9d0692cf0e 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 @@ -26,11 +26,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; 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.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** Implements the internal behavior of {@link ExoPlayerImpl}. */ @@ -53,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - Playlist.PlaylistInfoRefreshListener, + MediaSourceCaller, PlaybackParameterListener, PlayerMessage.Sender { @@ -72,21 +71,16 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SET_SEEK_PARAMETERS = 5; private static final int MSG_STOP = 6; private static final int MSG_RELEASE = 7; - private static final int MSG_PERIOD_PREPARED = 8; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; - private static final int MSG_SET_REPEAT_MODE = 11; - private static final int MSG_SET_SHUFFLE_ENABLED = 12; - private static final int MSG_SET_FOREGROUND_MODE = 13; - private static final int MSG_SEND_MESSAGE = 14; - private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; - private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16; - private static final int MSG_SET_MEDIA_ITEMS = 17; - private static final int MSG_ADD_MEDIA_ITEMS = 18; - private static final int MSG_MOVE_MEDIA_ITEMS = 19; - private static final int MSG_REMOVE_MEDIA_ITEMS = 20; - private static final int MSG_SET_SHUFFLE_ORDER = 21; - private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22; + private static final int MSG_REFRESH_SOURCE_INFO = 8; + private static final int MSG_PERIOD_PREPARED = 9; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; + private static final int MSG_SET_REPEAT_MODE = 12; + private static final int MSG_SET_SHUFFLE_ENABLED = 13; + private static final int MSG_SET_FOREGROUND_MODE = 14; + private static final int MSG_SEND_MESSAGE = 15; + private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; + private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -109,12 +103,12 @@ import java.util.concurrent.atomic.AtomicBoolean; private final ArrayList pendingMessages; private final Clock clock; private final MediaPeriodQueue queue; - private final Playlist playlist; @SuppressWarnings("unused") private SeekParameters seekParameters; private PlaybackInfo playbackInfo; + private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; private boolean playWhenReady; @@ -123,7 +117,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean shuffleModeEnabled; private boolean foregroundMode; - @Nullable private SeekPosition pendingInitialSeekPosition; + private int pendingPrepareCount; + private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private int nextPendingMessageIndex; @@ -136,7 +131,6 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, - @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock) { this.renderers = renderers; @@ -176,14 +170,12 @@ import java.util.concurrent.atomic.AtomicBoolean; new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = clock.createHandler(internalPlaybackThread.getLooper(), this); - playlist = new Playlist(this); - if (analyticsCollector != null) { - playlist.setAnalyticsCollector(eventHandler, analyticsCollector); - } } - public void prepare() { - handler.obtainMessage(MSG_PREPARE).sendToTarget(); + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + handler + .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource) + .sendToTarget(); } public void setPlayWhenReady(boolean playWhenReady) { @@ -216,62 +208,6 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void setMediaItems( - List mediaSources, ShuffleOrder shuffleOrder) { - setMediaItems( - mediaSources, - /* windowIndex= */ C.INDEX_UNSET, - /* positionUs= */ C.TIME_UNSET, - shuffleOrder); - } - - public void setMediaItems( - List mediaSources, - int windowIndex, - long positionUs, - ShuffleOrder shuffleOrder) { - handler - .obtainMessage( - MSG_SET_MEDIA_ITEMS, - new PlaylistUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs)) - .sendToTarget(); - } - - public void addMediaItems( - List mediaSources, ShuffleOrder shuffleOrder) { - addMediaItems(C.INDEX_UNSET, mediaSources, shuffleOrder); - } - - public void addMediaItems( - int index, List mediaSources, ShuffleOrder shuffleOrder) { - handler - .obtainMessage( - MSG_ADD_MEDIA_ITEMS, - index, - /* ignored */ 0, - new PlaylistUpdateMessage( - mediaSources, - shuffleOrder, - /* windowIndex= */ C.INDEX_UNSET, - /* positionUs= */ C.TIME_UNSET)) - .sendToTarget(); - } - - public void removeMediaItems(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { - handler.obtainMessage(MSG_REMOVE_MEDIA_ITEMS, fromIndex, toIndex, shuffleOrder).sendToTarget(); - } - - public void moveMediaItems( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - MoveMediaItemsMessage moveMediaItemsMessage = - new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder); - handler.obtainMessage(MSG_MOVE_MEDIA_ITEMS, moveMediaItemsMessage).sendToTarget(); - } - - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget(); - } - @Override public synchronized void sendMessage(PlayerMessage message) { if (released) { @@ -328,11 +264,13 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // Playlist.PlaylistInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override - public void onPlaylistUpdateRequested() { - handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + handler + .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) + .sendToTarget(); } // MediaPeriod.Callback implementation. @@ -364,12 +302,14 @@ import java.util.concurrent.atomic.AtomicBoolean; // Handler.Callback implementation. @Override - @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { try { switch (msg.what) { case MSG_PREPARE: - prepareInternal(); + prepareInternal( + (MediaSource) msg.obj, + /* resetPosition= */ msg.arg1 != 0, + /* resetState= */ msg.arg2 != 0); break; case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); @@ -405,6 +345,9 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); break; + case MSG_REFRESH_SOURCE_INFO: + handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); + break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); break; @@ -421,24 +364,6 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SEND_MESSAGE_TO_TARGET_THREAD: sendMessageToTargetThread((PlayerMessage) msg.obj); break; - case MSG_SET_MEDIA_ITEMS: - setMediaItemsInternal((PlaylistUpdateMessage) msg.obj); - break; - case MSG_ADD_MEDIA_ITEMS: - addMediaItemsInternal((PlaylistUpdateMessage) msg.obj, msg.arg1); - break; - case MSG_MOVE_MEDIA_ITEMS: - moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj); - break; - case MSG_REMOVE_MEDIA_ITEMS: - removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj); - break; - case MSG_SET_SHUFFLE_ORDER: - setShuffleOrderInternal((ShuffleOrder) msg.obj); - break; - case MSG_PLAYLIST_UPDATE_REQUESTED: - playlistUpdateRequestedInternal(); - break; case MSG_RELEASE: releaseInternal(); // Return immediately to not send playback info updates after release. @@ -508,77 +433,21 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void prepareInternal() { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); + private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + pendingPrepareCount++; resetInternal( /* resetRenderers= */ false, - /* resetPosition= */ false, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, + /* releaseMediaSource= */ true, + resetPosition, + resetState, /* resetError= */ true); loadControl.onPrepared(); - setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING); - playlist.prepare(bandwidthMeter.getTransferListener()); + this.mediaSource = mediaSource; + setState(Player.STATE_BUFFERING); + mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - private void setMediaItemsInternal(PlaylistUpdateMessage playlistUpdateMessage) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - if (playlistUpdateMessage.windowIndex != C.INDEX_UNSET) { - pendingInitialSeekPosition = - new SeekPosition( - new Playlist.PlaylistTimeline( - playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder), - playlistUpdateMessage.windowIndex, - playlistUpdateMessage.positionUs); - } - Timeline timeline = - playlist.setMediaSources( - playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void addMediaItemsInternal(PlaylistUpdateMessage addMessage, int insertionIndex) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = - playlist.addMediaSources( - insertionIndex == C.INDEX_UNSET ? playlist.getSize() : insertionIndex, - addMessage.mediaSourceHolders, - addMessage.shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = - playlist.moveMediaSourceRange( - moveMediaItemsMessage.fromIndex, - moveMediaItemsMessage.toIndex, - moveMediaItemsMessage.newFromIndex, - moveMediaItemsMessage.shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) - throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = playlist.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - - private void playlistUpdateRequestedInternal() throws ExoPlaybackException { - handlePlaylistInfoRefreshed(playlist.createTimeline()); - } - - private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException { - playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Timeline timeline = playlist.setShuffleOrder(shuffleOrder); - handlePlaylistInfoRefreshed(timeline); - } - private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { rebuffering = false; this.playWhenReady = playWhenReady; @@ -796,7 +665,6 @@ import java.util.concurrent.atomic.AtomicBoolean; long periodPositionUs; long contentPositionUs; boolean seekPositionAdjusted; - @Nullable Pair resolvedSeekPosition = resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (resolvedSeekPosition == null) { @@ -821,7 +689,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } try { - if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { + if (mediaSource == null || pendingPrepareCount > 0) { // Save seek position for later, as we are still waiting for a prepared source. pendingInitialSeekPosition = seekPosition; } else if (periodPositionUs == C.TIME_UNSET) { @@ -829,9 +697,9 @@ import java.util.concurrent.atomic.AtomicBoolean; setState(Player.STATE_ENDED); resetInternal( /* resetRenderers= */ false, + /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, + /* resetState= */ false, /* resetError= */ true); } else { // Execute the seek in the current media periods. @@ -978,11 +846,13 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { resetInternal( /* resetRenderers= */ forceResetRenderers || !foregroundMode, + /* releaseMediaSource= */ true, /* resetPosition= */ resetPositionAndState, - /* releasePlaylist= */ true, - /* clearPlaylist= */ resetPositionAndState, + /* resetState= */ resetPositionAndState, /* resetError= */ resetPositionAndState); - playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0); + playbackInfoUpdate.incrementPendingOperationAcks( + pendingPrepareCount + (acknowledgeStop ? 1 : 0)); + pendingPrepareCount = 0; loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -990,9 +860,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void releaseInternal() { resetInternal( /* resetRenderers= */ true, + /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* releasePlaylist= */ true, - /* clearPlaylist= */ true, + /* resetState= */ true, /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); @@ -1005,9 +875,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void resetInternal( boolean resetRenderers, + boolean releaseMediaSource, boolean resetPosition, - boolean releasePlaylist, - boolean clearPlaylist, + boolean resetState, boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; @@ -1035,8 +905,8 @@ import java.util.concurrent.atomic.AtomicBoolean; if (resetPosition) { pendingInitialSeekPosition = null; - } else if (clearPlaylist) { - // When clearing the playlist, also reset the period-based PlaybackInfo position and convert + } else if (resetState) { + // When resetting the state, also reset the period-based PlaybackInfo position and convert // existing position to initial seek instead. resetPosition = true; if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) { @@ -1047,10 +917,10 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist); + queue.clear(/* keepFrontPeriodUid= */ !resetState); setIsLoading(false); - if (clearPlaylist) { - queue.setTimeline(playlist.clear(/* shuffleOrder= */ null)); + if (resetState) { + queue.setTimeline(Timeline.EMPTY); for (PendingMessageInfo pendingMessageInfo : pendingMessages) { pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } @@ -1066,21 +936,24 @@ import java.util.concurrent.atomic.AtomicBoolean; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; playbackInfo = new PlaybackInfo( - clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? Timeline.EMPTY : playbackInfo.timeline, mediaPeriodId, startPositionUs, contentPositionUs, playbackInfo.playbackState, resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, - clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, - clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, mediaPeriodId, startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs); - if (releasePlaylist) { - playlist.release(); + if (releaseMediaSource) { + if (mediaSource != null) { + mediaSource.releaseSource(/* caller= */ this); + mediaSource = null; + } } } @@ -1088,7 +961,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (message.getPositionMs() == C.TIME_UNSET) { // If no delivery time is specified, trigger immediate message delivery. sendMessageToTarget(message); - } else if (playbackInfo.timeline.isEmpty()) { + } else if (mediaSource == null || pendingPrepareCount > 0) { // Still waiting for initial timeline to resolve position. pendingMessages.add(new PendingMessageInfo(message)); } else { @@ -1403,11 +1276,20 @@ import java.util.concurrent.atomic.AtomicBoolean; } } } - playlist.maybeThrowSourceInfoRefreshError(); + mediaSource.maybeThrowSourceInfoRefreshError(); } - private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException { + private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) + throws ExoPlaybackException { + if (sourceRefreshInfo.source != mediaSource) { + // Stale event. + return; + } + playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); + pendingPrepareCount = 0; + Timeline oldTimeline = playbackInfo.timeline; + Timeline timeline = sourceRefreshInfo.timeline; queue.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline); resolvePendingMessagePositions(); @@ -1418,7 +1300,6 @@ import java.util.concurrent.atomic.AtomicBoolean; long newContentPositionUs = oldContentPositionUs; if (pendingInitialSeekPosition != null) { // Resolve initial seek position. - @Nullable Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; @@ -1523,12 +1404,12 @@ import java.util.concurrent.atomic.AtomicBoolean; if (playbackInfo.playbackState != Player.STATE_IDLE) { setState(Player.STATE_ENDED); } - // Reset, but retain the playlist so that it can still be used should a seek occur. + // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* resetRenderers= */ false, + /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* releasePlaylist= */ false, - /* clearPlaylist= */ false, + /* resetState= */ false, /* resetError= */ true); } @@ -1571,7 +1452,6 @@ import java.util.concurrent.atomic.AtomicBoolean; * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - @Nullable private Pair resolveSeekPosition( SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; @@ -1628,9 +1508,13 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updatePeriods() throws ExoPlaybackException, IOException { - if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) { + if (mediaSource == null) { + // The player has no media source yet. + return; + } + if (pendingPrepareCount > 0) { // We're waiting to get information about periods. - playlist.maybeThrowSourceInfoRefreshError(); + mediaSource.maybeThrowSourceInfoRefreshError(); return; } maybeUpdateLoadingPeriod(); @@ -1650,7 +1534,7 @@ import java.util.concurrent.atomic.AtomicBoolean; rendererCapabilities, trackSelector, loadControl.getAllocator(), - playlist, + mediaSource, info, emptyTrackSelectorResult); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); @@ -1661,7 +1545,7 @@ import java.util.concurrent.atomic.AtomicBoolean; handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } } - @Nullable MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (!playbackInfo.isLoading) { @@ -1670,7 +1554,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void maybeUpdateReadingPeriod() throws ExoPlaybackException { - @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; } @@ -2079,38 +1963,14 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private static final class PlaylistUpdateMessage { + private static final class MediaSourceRefreshInfo { - private final List mediaSourceHolders; - private final ShuffleOrder shuffleOrder; - private final int windowIndex; - private final long positionUs; + public final MediaSource source; + public final Timeline timeline; - private PlaylistUpdateMessage( - List mediaSourceHolders, - ShuffleOrder shuffleOrder, - int windowIndex, - long positionUs) { - this.mediaSourceHolders = mediaSourceHolders; - this.shuffleOrder = shuffleOrder; - this.windowIndex = windowIndex; - this.positionUs = positionUs; - } - } - - private static class MoveMediaItemsMessage { - - public final int fromIndex; - public final int toIndex; - public final int newFromIndex; - public final ShuffleOrder shuffleOrder; - - public MoveMediaItemsMessage( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - this.fromIndex = fromIndex; - this.toIndex = toIndex; - this.newFromIndex = newFromIndex; - this.shuffleOrder = shuffleOrder; + public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { + this.source = source; + this.timeline = timeline; } } @@ -2119,7 +1979,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private PlaybackInfo lastPlaybackInfo; private int operationAcks; private boolean positionDiscontinuity; - @DiscontinuityReason private int discontinuityReason; + private @DiscontinuityReason int discontinuityReason; public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; @@ -2147,4 +2007,5 @@ import java.util.concurrent.atomic.AtomicBoolean; this.discontinuityReason = discontinuityReason; } } + } 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 5bbbcbea2a..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 @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; 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.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final boolean[] mayRetainStreamFlags; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; - private final Playlist playlist; + private final MediaSource mediaSource; @Nullable private MediaPeriodHolder next; private TrackGroupArray trackGroups; @@ -69,7 +70,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. - * @param playlist The playlist. + * @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. @@ -79,13 +80,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, - Playlist playlist, + MediaSource mediaSource, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; - this.playlist = playlist; + this.mediaSource = mediaSource; this.uid = info.id.periodUid; this.info = info; this.trackGroups = TrackGroupArray.EMPTY; @@ -93,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = - createMediaPeriod(info.id, playlist, allocator, info.startPositionUs, info.endPositionUs); + createMediaPeriod( + info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); } /** @@ -303,7 +305,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod); + releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); } /** @@ -400,11 +402,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( MediaPeriodId id, - Playlist playlist, + MediaSource mediaSource, Allocator allocator, long startPositionUs, long endPositionUs) { - MediaPeriod mediaPeriod = playlist.createPeriod(id, allocator, startPositionUs); + MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( @@ -415,12 +417,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ private static void releaseMediaPeriod( - long endPositionUs, Playlist playlist, MediaPeriod mediaPeriod) { + long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) { try { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { - playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { - playlist.releasePeriod(mediaPeriod); + mediaSource.releasePeriod(mediaPeriod); } } catch (RuntimeException e) { // There's nothing we can do. 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 5b39db54aa..901b7b4d94 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 @@ -19,6 +19,7 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.RepeatMode; 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; @@ -133,7 +134,7 @@ import com.google.android.exoplayer2.util.Assertions; * @param rendererCapabilities The renderer capabilities. * @param trackSelector The track selector. * @param allocator The allocator. - * @param playlist The playlist. + * @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. @@ -142,7 +143,7 @@ import com.google.android.exoplayer2.util.Assertions; RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, - Playlist playlist, + MediaSource mediaSource, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = @@ -157,7 +158,7 @@ import com.google.android.exoplayer2.util.Assertions; rendererPositionOffsetUs, trackSelector, allocator, - playlist, + mediaSource, info, emptyTrackSelectorResult); if (loading != null) { 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 b9ab69c45f..fafbd25c32 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 @@ -356,8 +356,7 @@ public interface Player { * {@link #onPositionDiscontinuity(int)}. * * @param timeline The latest timeline. Never null, but may be empty. - * @param manifest The latest manifest in case the timeline has a single window only. Always - * null if the timeline has more than a single window. + * @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, @@ -585,17 +584,25 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link - * #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}. + * 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) - @IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE}) + @IntDef({ + TIMELINE_CHANGE_REASON_PREPARED, + TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC + }) @interface TimelineChangeReason {} - /** Timeline changed as a result of a change of the playlist items or the order of the items. */ - int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0; - /** Timeline changed as a result of a dynamic update introduced by the played media. */ - int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; + /** Timeline and manifest changed as a result of a player initialization with new media. */ + int TIMELINE_CHANGE_REASON_PREPARED = 0; + /** Timeline and manifest changed as a result of a player reset. */ + int TIMELINE_CHANGE_REASON_RESET = 1; + /** + * Timeline or manifest changed as a result of an dynamic update introduced by the played media. + */ + int TIMELINE_CHANGE_REASON_DYNAMIC = 2; /** Returns the component of this player for audio output, or null if audio is not supported. */ @Nullable diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java deleted file mode 100644 index c5476a151b..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java +++ /dev/null @@ -1,708 +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; - -import android.os.Handler; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; -import com.google.android.exoplayer2.source.MaskingMediaPeriod; -import com.google.android.exoplayer2.source.MaskingMediaSource; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -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; - -/** - * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified - * during playback. It is valid for the same {@link MediaSource} instance to be present more than - * once in the playlist. - * - *

        With the exception of the constructor, all methods are called on the playback thread. - */ -/* package */ class Playlist { - - /** Listener for source events. */ - public interface PlaylistInfoRefreshListener { - - /** - * Called when the timeline of a media item has changed and a new timeline that reflects the - * current playlist state needs to be created by calling {@link #createTimeline()}. - * - *

        Called on the playback thread. - */ - void onPlaylistUpdateRequested(); - } - - private final List mediaSourceHolders; - private final Map mediaSourceByMediaPeriod; - private final Map mediaSourceByUid; - private final PlaylistInfoRefreshListener playlistInfoListener; - private final MediaSourceEventListener.EventDispatcher eventDispatcher; - private final HashMap childSources; - private final Set enabledMediaSourceHolders; - - private ShuffleOrder shuffleOrder; - private boolean isPrepared; - - @Nullable private TransferListener mediaTransferListener; - - @SuppressWarnings("initialization") - public Playlist(PlaylistInfoRefreshListener listener) { - playlistInfoListener = listener; - shuffleOrder = new DefaultShuffleOrder(0); - mediaSourceByMediaPeriod = new IdentityHashMap<>(); - mediaSourceByUid = new HashMap<>(); - mediaSourceHolders = new ArrayList<>(); - eventDispatcher = new MediaSourceEventListener.EventDispatcher(); - childSources = new HashMap<>(); - enabledMediaSourceHolders = new HashSet<>(); - } - - /** - * Sets the media sources replacing any sources previously contained in the playlist. - * - * @param holders The list of {@link MediaSourceHolder}s to set. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - */ - public final Timeline setMediaSources( - List holders, ShuffleOrder shuffleOrder) { - removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); - return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder); - } - - /** - * Adds multiple {@link MediaSourceHolder}s to the playlist. - * - * @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index - * must be in the range of 0 <= index <= {@link #getSize()}. - * @param holders A list of {@link MediaSourceHolder}s to be added. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - */ - public final Timeline addMediaSources( - int index, List holders, ShuffleOrder shuffleOrder) { - if (!holders.isEmpty()) { - this.shuffleOrder = shuffleOrder; - for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) { - MediaSourceHolder holder = holders.get(insertionIndex - index); - if (insertionIndex > 0) { - MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1); - Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); - holder.reset( - /* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild - + previousTimeline.getWindowCount()); - } else { - holder.reset(/* firstWindowIndexInChild= */ 0); - } - Timeline newTimeline = holder.mediaSource.getTimeline(); - correctOffsets( - /* startIndex= */ insertionIndex, - /* windowOffsetUpdate= */ newTimeline.getWindowCount()); - mediaSourceHolders.add(insertionIndex, holder); - mediaSourceByUid.put(holder.uid, holder); - if (isPrepared) { - prepareChildSource(holder); - if (mediaSourceByMediaPeriod.isEmpty()) { - enabledMediaSourceHolders.add(holder); - } else { - disableChildSource(holder); - } - } - } - } - return createTimeline(); - } - - /** - * Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index - * (included) and a final index (excluded). - * - *

        Note: when specified range is empty, no actual media source is removed and no exception is - * thrown. - * - * @param fromIndex The initial range index, pointing to the first media source that will be - * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param toIndex The final range index, pointing to the first media source that will be left - * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, - * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} - */ - public final Timeline removeMediaSourceRange( - int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { - Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize()); - this.shuffleOrder = shuffleOrder; - removeMediaSourcesInternal(fromIndex, toIndex); - return createTimeline(); - } - - /** - * Moves an existing media source within the playlist. - * - * @param currentIndex The current index of the media source in the playlist. This index must be - * in the range of 0 <= index < {@link #getSize()}. - * @param newIndex The target index of the media source in the playlist. This index must be in the - * range of 0 <= index < {@link #getSize()}. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0, - * {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0 - */ - public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) { - return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder); - } - - /** - * Moves a range of media sources within the playlist. - * - *

        Note: when specified range is empty or the from index equals the new from index, no actual - * media source is moved and no exception is thrown. - * - * @param fromIndex The initial range index, pointing to the first media source of the range that - * will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param toIndex The final range index, pointing to the first media source that will be left - * untouched. This index must be larger or equals than {@code fromIndex}. - * @param newFromIndex The target index of the first media source of the range that will be moved. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, - * {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code - * newFromIndex} < 0 - */ - public Timeline moveMediaSourceRange( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - Assertions.checkArgument( - fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0); - this.shuffleOrder = shuffleOrder; - if (fromIndex == toIndex || fromIndex == newFromIndex) { - return createTimeline(); - } - int startIndex = Math.min(fromIndex, newFromIndex); - int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1; - int endIndex = Math.max(newEndIndex, toIndex - 1); - int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; - moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); - for (int i = startIndex; i <= endIndex; i++) { - MediaSourceHolder holder = mediaSourceHolders.get(i); - holder.firstWindowIndexInChild = windowOffset; - windowOffset += holder.mediaSource.getTimeline().getWindowCount(); - } - return createTimeline(); - } - - /** Clears the playlist. */ - public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) { - this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear(); - removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize()); - return createTimeline(); - } - - /** Whether the playlist is prepared. */ - public final boolean isPrepared() { - return isPrepared; - } - - /** Returns the number of media sources in the playlist. */ - public final int getSize() { - return mediaSourceHolders.size(); - } - - /** - * Sets the {@link AnalyticsCollector}. - * - * @param handler The handler on which to call the collector. - * @param analyticsCollector The analytics collector. - */ - public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) { - eventDispatcher.addEventListener(handler, analyticsCollector); - } - - /** - * Sets a new shuffle order to use when shuffling the child media sources. - * - * @param shuffleOrder A {@link ShuffleOrder}. - */ - public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) { - int size = getSize(); - if (shuffleOrder.getLength() != size) { - shuffleOrder = - shuffleOrder - .cloneAndClear() - .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); - } - this.shuffleOrder = shuffleOrder; - return createTimeline(); - } - - /** Prepares the playlist. */ - public final void prepare(@Nullable TransferListener mediaTransferListener) { - Assertions.checkState(!isPrepared); - this.mediaTransferListener = mediaTransferListener; - for (int i = 0; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - prepareChildSource(mediaSourceHolder); - enabledMediaSourceHolders.add(mediaSourceHolder); - } - isPrepared = true; - } - - /** - * Returns a new {@link MediaPeriod} identified by {@code periodId}. - * - * @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. - * @return A new {@link MediaPeriod}. - */ - public MediaPeriod createPeriod( - MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) { - Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); - MediaSource.MediaPeriodId childMediaPeriodId = - id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); - MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); - enableMediaSource(holder); - holder.activeMediaPeriodIds.add(childMediaPeriodId); - MediaPeriod mediaPeriod = - holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); - mediaSourceByMediaPeriod.put(mediaPeriod, holder); - disableUnusedMediaSources(); - return mediaPeriod; - } - - /** - * Releases the period. - * - * @param mediaPeriod The period to release. - */ - public final void releasePeriod(MediaPeriod mediaPeriod) { - MediaSourceHolder holder = - Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); - holder.mediaSource.releasePeriod(mediaPeriod); - holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); - if (!mediaSourceByMediaPeriod.isEmpty()) { - disableUnusedMediaSources(); - } - maybeReleaseChildSource(holder); - } - - /** Releases the playlist. */ - public final void release() { - for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.caller); - childSource.mediaSource.removeEventListener(childSource.eventListener); - } - childSources.clear(); - enabledMediaSourceHolders.clear(); - isPrepared = false; - } - - /** Throws any pending error encountered while loading or refreshing. */ - public final void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - /** Creates a timeline reflecting the current state of the playlist. */ - public final Timeline createTimeline() { - if (mediaSourceHolders.isEmpty()) { - return Timeline.EMPTY; - } - int windowOffset = 0; - for (int i = 0; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - mediaSourceHolder.firstWindowIndexInChild = windowOffset; - windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount(); - } - return new PlaylistTimeline(mediaSourceHolders, shuffleOrder); - } - - // Internal methods. - - private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { - enabledMediaSourceHolders.add(mediaSourceHolder); - @Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder); - if (enabledChild != null) { - enabledChild.mediaSource.enable(enabledChild.caller); - } - } - - private void disableUnusedMediaSources() { - Iterator iterator = enabledMediaSourceHolders.iterator(); - while (iterator.hasNext()) { - MediaSourceHolder holder = iterator.next(); - if (holder.activeMediaPeriodIds.isEmpty()) { - disableChildSource(holder); - iterator.remove(); - } - } - } - - private void disableChildSource(MediaSourceHolder holder) { - @Nullable MediaSourceAndListener disabledChild = childSources.get(holder); - if (disabledChild != null) { - disabledChild.mediaSource.disable(disabledChild.caller); - } - } - - private void removeMediaSourcesInternal(int fromIndex, int toIndex) { - for (int index = toIndex - 1; index >= fromIndex; index--) { - MediaSourceHolder holder = mediaSourceHolders.remove(index); - mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.mediaSource.getTimeline(); - correctOffsets( - /* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount()); - holder.isRemoved = true; - if (isPrepared) { - maybeReleaseChildSource(holder); - } - } - } - - private void correctOffsets(int startIndex, int windowOffsetUpdate) { - for (int i = startIndex; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate; - } - } - - // Internal methods to manage child sources. - - @Nullable - private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) { - for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) { - // Ensure the reported media period id has the same window sequence number as the one created - // by this media source. Otherwise it does not belong to this child source. - if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber - == mediaPeriodId.windowSequenceNumber) { - Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); - return mediaPeriodId.copyWithPeriodUid(periodUid); - } - } - return null; - } - - private static int getWindowIndexForChildWindowIndex( - MediaSourceHolder mediaSourceHolder, int windowIndex) { - return windowIndex + mediaSourceHolder.firstWindowIndexInChild; - } - - private void prepareChildSource(MediaSourceHolder holder) { - MediaSource mediaSource = holder.mediaSource; - MediaSource.MediaSourceCaller caller = - (source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested(); - MediaSourceEventListener eventListener = new ForwardingEventListener(holder); - childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); - mediaSource.addEventListener(new Handler(), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); - } - - private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { - // Release if the source has been removed from the playlist and no periods are still active. - if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { - MediaSourceAndListener removedChild = - Assertions.checkNotNull(childSources.remove(mediaSourceHolder)); - removedChild.mediaSource.releaseSource(removedChild.caller); - removedChild.mediaSource.removeEventListener(removedChild.eventListener); - enabledMediaSourceHolders.remove(mediaSourceHolder); - } - } - - /** Return uid of media source holder from period uid of concatenated source. */ - private static Object getMediaSourceHolderUid(Object periodUid) { - return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); - } - - /** Return uid of child period from period uid of concatenated source. */ - private static Object getChildPeriodUid(Object periodUid) { - return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); - } - - private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { - return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid); - } - - /* package */ static void moveMediaSourceHolders( - List mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) { - MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex]; - for (int i = removedItems.length - 1; i >= 0; i--) { - removedItems[i] = mediaSourceHolders.remove(fromIndex + i); - } - mediaSourceHolders.addAll( - Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems)); - } - - /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder { - - public final MaskingMediaSource mediaSource; - public final Object uid; - public final List activeMediaPeriodIds; - - public int firstWindowIndexInChild; - public boolean isRemoved; - - public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { - this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); - this.activeMediaPeriodIds = new ArrayList<>(); - this.uid = new Object(); - } - - public void reset(int firstWindowIndexInChild) { - this.firstWindowIndexInChild = firstWindowIndexInChild; - this.isRemoved = false; - this.activeMediaPeriodIds.clear(); - } - } - - /** Timeline exposing concatenated timelines of playlist media sources. */ - /* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline { - - private final int windowCount; - private final int periodCount; - private final int[] firstPeriodInChildIndices; - private final int[] firstWindowInChildIndices; - private final Timeline[] timelines; - private final Object[] uids; - private final HashMap childIndexByUid; - - public PlaylistTimeline( - Collection mediaSourceHolders, ShuffleOrder shuffleOrder) { - super(/* isAtomic= */ false, shuffleOrder); - int childCount = mediaSourceHolders.size(); - firstPeriodInChildIndices = new int[childCount]; - firstWindowInChildIndices = new int[childCount]; - timelines = new Timeline[childCount]; - uids = new Object[childCount]; - childIndexByUid = new HashMap<>(); - int index = 0; - int windowCount = 0; - int periodCount = 0; - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - 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 - protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); - } - - @Override - protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); - } - - @Override - protected int getChildIndexByChildUid(Object childUid) { - Integer index = childIndexByUid.get(childUid); - return index == null ? C.INDEX_UNSET : index; - } - - @Override - protected Timeline getTimelineByChildIndex(int childIndex) { - return timelines[childIndex]; - } - - @Override - protected int getFirstPeriodIndexByChildIndex(int childIndex) { - return firstPeriodInChildIndices[childIndex]; - } - - @Override - protected int getFirstWindowIndexByChildIndex(int childIndex) { - return firstWindowInChildIndices[childIndex]; - } - - @Override - protected Object getChildUidByChildIndex(int childIndex) { - return uids[childIndex]; - } - - @Override - public int getWindowCount() { - return windowCount; - } - - @Override - public int getPeriodCount() { - return periodCount; - } - } - - private static final class MediaSourceAndListener { - - public final MediaSource mediaSource; - public final MediaSource.MediaSourceCaller caller; - public final MediaSourceEventListener eventListener; - - public MediaSourceAndListener( - MediaSource mediaSource, - MediaSource.MediaSourceCaller caller, - MediaSourceEventListener eventListener) { - this.mediaSource = mediaSource; - this.caller = caller; - this.eventListener = eventListener; - } - } - - private final class ForwardingEventListener implements MediaSourceEventListener { - - private final Playlist.MediaSourceHolder id; - private EventDispatcher eventDispatcher; - - public ForwardingEventListener(Playlist.MediaSourceHolder id) { - eventDispatcher = Playlist.this.eventDispatcher; - this.id = id; - } - - @Override - public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodCreated(); - } - } - - @Override - public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodReleased(); - } - } - - @Override - public void onLoadStarted( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadStarted(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadCompleted( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadCompleted(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadCanceled( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadCanceled(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadError( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData, - IOException error, - boolean wasCanceled) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled); - } - } - - @Override - public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.readingStarted(); - } - } - - @Override - public void onUpstreamDiscarded( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.upstreamDiscarded(mediaLoadData); - } - } - - @Override - public void onDownstreamFormatChanged( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.downstreamFormatChanged(mediaLoadData); - } - } - - /** Updates the event dispatcher and returns whether the event should be dispatched. */ - private boolean maybeUpdateEventDispatcher( - int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { - @Nullable MediaSource.MediaPeriodId mediaPeriodId = null; - if (childMediaPeriodId != null) { - mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); - if (mediaPeriodId == null) { - // Media period not found. Ignore event. - return false; - } - } - int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); - if (eventDispatcher.windowIndex != windowIndex - || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { - eventDispatcher = - Playlist.this.eventDispatcher.withParameters( - windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L); - } - return true; - } - } -} - 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 de9802357c..43a5ebab99 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 @@ -43,7 +43,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; @@ -164,9 +163,7 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter A {@link BandwidthMeter}. * @param looper A {@link Looper} that must be used for all calls to the player. * @param analyticsCollector An {@link AnalyticsCollector}. - * @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all - * initial preparation steps (e.g., manifest loads) happen immediately. If true, these - * initial preparations are triggered only when the player starts buffering the media. + * @param useLazyPreparation Whether media sources should be initialized lazily. * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. */ public Builder( @@ -303,7 +300,6 @@ public class SimpleExoPlayer extends BasePlayer loadControl, bandwidthMeter, analyticsCollector, - useLazyPreparation, clock, looper); } @@ -343,6 +339,7 @@ public class SimpleExoPlayer extends BasePlayer private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; + @Nullable private MediaSource mediaSource; private List currentCues; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private CameraMotionListener cameraMotionListener; @@ -358,9 +355,6 @@ public class SimpleExoPlayer extends BasePlayer * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will * collect and forward all player events. - * @param useLazyPreparation Whether playlist items are 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. * @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 @@ -374,7 +368,6 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { this( @@ -385,14 +378,26 @@ public class SimpleExoPlayer extends BasePlayer DrmSessionManager.getDummyDrmSessionManager(), bandwidthMeter, analyticsCollector, - useLazyPreparation, clock, looper); } /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @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 bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @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 + * used to call listeners on. * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, - * BandwidthMeter, AnalyticsCollector, boolean, Clock, Looper)} instead, and pass the {@link + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link * DrmSessionManager} to the {@link MediaSource} factories. */ @Deprecated @@ -404,7 +409,6 @@ public class SimpleExoPlayer extends BasePlayer @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; @@ -435,15 +439,7 @@ public class SimpleExoPlayer extends BasePlayer // Build the player and associated objects. player = - new ExoPlayerImpl( - renderers, - trackSelector, - loadControl, - bandwidthMeter, - analyticsCollector, - useLazyPreparation, - clock, - looper); + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); analyticsCollector.setPlayer(player); addListener(analyticsCollector); addListener(componentListener); @@ -1103,133 +1099,32 @@ public class SimpleExoPlayer extends BasePlayer } @Override - @Deprecated public void retry() { verifyApplicationThread(); - prepare(); + if (mediaSource != null + && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } } @Override - public void prepare() { - verifyApplicationThread(); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepare(); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") public void prepare(MediaSource mediaSource) { prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override - @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { verifyApplicationThread(); - setMediaItems( - Collections.singletonList(mediaSource), - /* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET, - /* startPositionMs= */ C.TIME_UNSET); - prepare(); - } - - @Override - public void setMediaItems(List mediaItems) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems); - } - - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems, resetPosition); - } - - @Override - public void setMediaItems( - List mediaItems, int startWindowIndex, long startPositionMs) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItems(mediaItems, startWindowIndex, startPositionMs); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - verifyApplicationThread(); - analyticsCollector.resetForNewPlaylist(); - player.setMediaItem(mediaItem, startPositionMs); - } - - @Override - public void addMediaItem(MediaSource mediaSource) { - verifyApplicationThread(); - player.addMediaItem(mediaSource); - } - - @Override - public void addMediaItem(int index, MediaSource mediaSource) { - verifyApplicationThread(); - player.addMediaItem(index, mediaSource); - } - - @Override - public void addMediaItems(List mediaSources) { - verifyApplicationThread(); - player.addMediaItems(mediaSources); - } - - @Override - public void addMediaItems(int index, List mediaSources) { - verifyApplicationThread(); - player.addMediaItems(index, mediaSources); - } - - @Override - public void moveMediaItem(int currentIndex, int newIndex) { - verifyApplicationThread(); - player.moveMediaItem(currentIndex, newIndex); - } - - @Override - public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - verifyApplicationThread(); - player.moveMediaItems(fromIndex, toIndex, newIndex); - } - - @Override - public MediaSource removeMediaItem(int index) { - verifyApplicationThread(); - return player.removeMediaItem(index); - } - - @Override - public void removeMediaItems(int fromIndex, int toIndex) { - verifyApplicationThread(); - player.removeMediaItems(fromIndex, toIndex); - } - - @Override - public void clearMediaItems() { - verifyApplicationThread(); - player.clearMediaItems(); - } - - @Override - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - verifyApplicationThread(); - player.setShuffleOrder(shuffleOrder); + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + this.mediaSource = mediaSource; + mediaSource.addEventListener(eventHandler, analyticsCollector); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + player.prepare(mediaSource, resetPosition, resetState); } @Override @@ -1309,7 +1204,6 @@ public class SimpleExoPlayer extends BasePlayer @Override public void setForegroundMode(boolean foregroundMode) { - verifyApplicationThread(); player.setForegroundMode(foregroundMode); } @@ -1317,6 +1211,13 @@ public class SimpleExoPlayer extends BasePlayer public void stop(boolean reset) { verifyApplicationThread(); player.stop(reset); + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + if (reset) { + mediaSource = null; + } + } audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @@ -1333,6 +1234,10 @@ public class SimpleExoPlayer extends BasePlayer } surface = null; } + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + mediaSource = null; + } if (isPriorityTaskManagerRegistered) { Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); isPriorityTaskManagerRegistered = false; 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 458532c86d..c496052f94 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 @@ -19,7 +19,6 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; /** * A flexible representation of the structure of media. A timeline is able to represent the @@ -271,46 +270,6 @@ public abstract class Timeline { return positionInFirstPeriodUs; } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !getClass().equals(obj.getClass())) { - return false; - } - Window that = (Window) obj; - return Util.areEqual(uid, that.uid) - && Util.areEqual(tag, that.tag) - && Util.areEqual(manifest, that.manifest) - && presentationStartTimeMs == that.presentationStartTimeMs - && windowStartTimeMs == that.windowStartTimeMs - && isSeekable == that.isSeekable - && isDynamic == that.isDynamic - && defaultPositionUs == that.defaultPositionUs - && durationUs == that.durationUs - && firstPeriodIndex == that.firstPeriodIndex - && lastPeriodIndex == that.lastPeriodIndex - && positionInFirstPeriodUs == that.positionInFirstPeriodUs; - } - - @Override - public int hashCode() { - int result = 7; - result = 31 * result + uid.hashCode(); - result = 31 * result + (tag == null ? 0 : tag.hashCode()); - result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); - result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); - result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); - result = 31 * result + (isSeekable ? 1 : 0); - result = 31 * result + (isDynamic ? 1 : 0); - result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + firstPeriodIndex; - result = 31 * result + lastPeriodIndex; - result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); - return result; - } } /** @@ -567,34 +526,6 @@ public abstract class Timeline { return adPlaybackState.adResumePositionUs; } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !getClass().equals(obj.getClass())) { - return false; - } - Period that = (Period) obj; - return Util.areEqual(id, that.id) - && Util.areEqual(uid, that.uid) - && windowIndex == that.windowIndex - && durationUs == that.durationUs - && positionInWindowUs == that.positionInWindowUs - && Util.areEqual(adPlaybackState, that.adPlaybackState); - } - - @Override - public int hashCode() { - int result = 7; - result = 31 * result + (id == null ? 0 : id.hashCode()); - result = 31 * result + (uid == null ? 0 : uid.hashCode()); - result = 31 * result + windowIndex; - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); - result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode()); - return result; - } } /** An empty timeline. */ 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 2cb160d092..43154a4b3f 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 @@ -132,8 +132,11 @@ public class AnalyticsCollector } } - /** Resets the analytics collector for a new playlist. */ - public final void resetForNewPlaylist() { + /** + * Resets the analytics collector for a new media source. Should be called before the player is + * prepared with a new media source. + */ + public final void resetForNewMediaSource() { // Copying the list is needed because onMediaPeriodReleased will modify the list. List mediaPeriodInfos = new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); @@ -783,13 +786,9 @@ public class AnalyticsCollector /** Updates the queue with a newly created media period. */ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); - boolean isInTimeline = periodIndex != C.INDEX_UNSET; + boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET; MediaPeriodInfo mediaPeriodInfo = - new MediaPeriodInfo( - mediaPeriodId, - isInTimeline ? timeline : Timeline.EMPTY, - isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); + new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); @@ -805,7 +804,7 @@ public class AnalyticsCollector public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); if (mediaPeriodInfo == null) { - // The media period has already been removed from the queue in resetForNewPlaylist(). + // The media period has already been removed from the queue in resetForNewMediaSource(). return false; } mediaPeriodInfoQueue.remove(mediaPeriodInfo); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java similarity index 89% rename from library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index a307e4b35d..29ef1faa80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2; +package com.google.android.exoplayer2.source; import android.util.Pair; -import com.google.android.exoplayer2.source.ShuffleOrder; +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. */ -public abstract class AbstractConcatenatedTimeline extends Timeline { +/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; private final ShuffleOrder shuffleOrder; @@ -74,8 +76,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled) { + public int getNextWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { if (isAtomic) { // Adapt repeat and shuffle mode to atomic concatenation. repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; @@ -84,10 +86,12 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); - int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( - windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, - shuffleModeEnabled); + int nextWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getNextWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } @@ -108,8 +112,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled) { + public int getPreviousWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { if (isAtomic) { // Adapt repeat and shuffle mode to atomic concatenation. repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; @@ -118,10 +122,12 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); - int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( - windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, - shuffleModeEnabled); + int previousWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getPreviousWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } @@ -219,8 +225,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { int childIndex = getChildIndexByPeriodIndex(periodIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); - getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, - setIds); + getTimelineByChildIndex(childIndex) + .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { period.uid = @@ -242,7 +248,8 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { return C.INDEX_UNSET; } int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); - return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET + return periodIndexInChild == C.INDEX_UNSET + ? C.INDEX_UNSET : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; } @@ -307,13 +314,14 @@ public abstract class AbstractConcatenatedTimeline extends Timeline { protected abstract Object getChildUidByChildIndex(int childIndex); private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + return shuffleModeEnabled + ? shuffleOrder.getNextIndex(childIndex) : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; } private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + return shuffleModeEnabled + ? shuffleOrder.getPreviousIndex(childIndex) : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; } - } 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 cfd0ad9377..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 @@ -19,7 +19,6 @@ import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; 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 68bed250e8..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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; 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 index 33bbf795be..8727fc5ed9 100644 --- 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import android.util.Pair; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; @@ -62,7 +61,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Returns the {@link Timeline}. */ - public synchronized Timeline getTimeline() { + public Timeline getTimeline() { return timeline; } @@ -130,7 +129,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - protected synchronized void onChildSourceInfoRefreshed( + protected void onChildSourceInfoRefreshed( Void id, MediaSource mediaSource, Timeline newTimeline) { if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); @@ -294,8 +293,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ - @VisibleForTesting - public static final class DummyTimeline extends Timeline { + private static final class DummyTimeline extends Timeline { @Nullable private final Object tag; @@ -334,8 +332,8 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { return period.set( - /* id= */ setIds ? 0 : null, - /* uid= */ setIds ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : null, + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID, /* windowIndex= */ 0, /* durationUs = */ C.TIME_UNSET, /* positionInWindowUs= */ 0); 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 66e78eb3a5..c37e98776e 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 @@ -594,10 +594,12 @@ public class EventLogger implements AnalyticsListener { private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { switch (reason) { - case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE: - return "SOURCE_UPDATE"; - case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED: - return "PLAYLIST_CHANGED"; + case Player.TIMELINE_CHANGE_REASON_PREPARED: + return "PREPARED"; + case Player.TIMELINE_CHANGE_REASON_RESET: + return "RESET"; + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + return "DYNAMIC"; default: return "?"; } 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 cbd246cf22..e11aa53b0f 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 @@ -53,7 +53,6 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SeekParameters; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -2025,42 +2024,6 @@ public final class Util { } } - /** - * Checks whether the timelines are the same. - * - * @param firstTimeline The first {@link Timeline}. - * @param secondTimeline The second {@link Timeline} to compare with. - * @return {@code true} if the both timelines are the same. - */ - public static boolean areTimelinesSame(Timeline firstTimeline, Timeline secondTimeline) { - if (firstTimeline == secondTimeline) { - return true; - } - if (secondTimeline.getWindowCount() != firstTimeline.getWindowCount() - || secondTimeline.getPeriodCount() != firstTimeline.getPeriodCount()) { - return false; - } - Timeline.Window firstWindow = new Timeline.Window(); - Timeline.Period firstPeriod = new Timeline.Period(); - Timeline.Window secondWindow = new Timeline.Window(); - Timeline.Period secondPeriod = new Timeline.Period(); - for (int i = 0; i < firstTimeline.getWindowCount(); i++) { - if (!firstTimeline - .getWindow(i, firstWindow) - .equals(secondTimeline.getWindow(i, secondWindow))) { - return false; - } - } - for (int i = 0; i < firstTimeline.getPeriodCount(); i++) { - if (!firstTimeline - .getPeriod(i, firstPeriod, /* setIds= */ true) - .equals(secondTimeline.getPeriod(i, secondPeriod, /* setIds= */ true))) { - return false; - } - } - return true; - } - private static HashMap createIso3ToIso2Map() { String[] iso2Languages = Locale.getISOLanguages(); HashMap iso3ToIso2 = 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 ee51a28052..37c026db74 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.fail; import android.content.Context; @@ -32,7 +31,6 @@ 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.MaskingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -87,12 +85,10 @@ public final class ExoPlayerTest { private static final int TIMEOUT_MS = 10000; private Context context; - private Timeline dummyTimeline; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); - dummyTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ 0); } /** @@ -102,7 +98,6 @@ public final class ExoPlayerTest { @Test public void testPlayEmptyTimeline() throws Exception { Timeline timeline = Timeline.EMPTY; - Timeline expectedMaskingTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); FakeRenderer renderer = new FakeRenderer(); ExoPlayerTestRunner testRunner = new Builder() @@ -112,10 +107,7 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesSame(expectedMaskingTimeline, Timeline.EMPTY); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); assertThat(renderer.formatReadCount).isEqualTo(0); assertThat(renderer.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.isEnded).isFalse(); @@ -136,10 +128,8 @@ public final class ExoPlayerTest { .start() .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.formatReadCount).isEqualTo(1); assertThat(renderer.sampleBufferReadCount).isEqualTo(1); @@ -161,10 +151,8 @@ public final class ExoPlayerTest { testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertThat(renderer.formatReadCount).isEqualTo(3); assertThat(renderer.sampleBufferReadCount).isEqualTo(3); assertThat(renderer.isEnded).isTrue(); @@ -187,10 +175,8 @@ public final class ExoPlayerTest { Integer[] expectedReasons = new Integer[99]; Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertThat(renderer.formatReadCount).isEqualTo(100); assertThat(renderer.sampleBufferReadCount).isEqualTo(100); assertThat(renderer.isEnded).isTrue(); @@ -262,17 +248,14 @@ public final class ExoPlayerTest { testRunner.assertPositionDiscontinuityReasonsEqual( Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); assertThat(audioRenderer.positionResetCount).isEqualTo(1); assertThat(videoRenderer.isEnded).isTrue(); assertThat(audioRenderer.isEnded).isTrue(); } @Test - public void testResettingMediaItemsGivesFreshSourceInfo() throws Exception { + public void testRepreparationGivesFreshSourceInfo() throws Exception { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); @@ -288,8 +271,8 @@ public final class ExoPlayerTest { @Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); // We've queued a source info refresh on the playback thread's event queue. Allow the - // test thread to set the third source to the playlist, and block this thread (the - // playback thread) until the test thread's call to setMediaItems() has returned. + // test thread to prepare the player with the third source, and block this thread (the + // playback thread) until the test thread's call to prepare() has returned. queuedSourceInfoCountDownLatch.countDown(); try { completePreparationCountDownLatch.await(); @@ -304,13 +287,12 @@ public final class ExoPlayerTest { // 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 - // test thread to set a third source, and block the playback thread until the test thread's call - // to setMediaItems() has returned. + // test thread to prepare the player with a third source, and block the playback thread until + // the test thread's call to prepare() has returned. ActionSchedule actionSchedule = - new ActionSchedule.Builder("testResettingMediaItemsGivesFreshSourceInfo") - .waitForTimelineChanged( - firstTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .setMediaItems(secondSource) + new ActionSchedule.Builder("testRepreparation") + .waitForTimelineChanged(firstTimeline) + .prepareSource(secondSource) .executeRunnable( () -> { try { @@ -319,32 +301,26 @@ public final class ExoPlayerTest { // Ignore. } }) - .setMediaItems(thirdSource) + .prepareSource(thirdSource) .executeRunnable(completePreparationCountDownLatch::countDown) - .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSources(firstSource) + .setMediaSource(firstSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - // The first source's preparation completed with a real timeline. When the second source was - // prepared, it immediately exposed a dummy timeline, but the source info refresh from the - // second source was suppressed as we replace it with the third source before the update - // arrives. - testRunner.assertTimelinesSame( - dummyTimeline, firstTimeline, dummyTimeline, dummyTimeline, thirdTimeline); + // 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(firstTimeline, Timeline.EMPTY, thirdTimeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.isEnded).isTrue(); } @@ -356,8 +332,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .playUntilStartOfWindow(/* windowIndex= */ 1) .setRepeatMode(Player.REPEAT_MODE_ONE) .playUntilStartOfWindow(/* windowIndex= */ 1) @@ -392,10 +367,8 @@ public final class ExoPlayerTest { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertThat(renderer.isEnded).isTrue(); } @@ -424,7 +397,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) @@ -470,13 +443,12 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_READY) .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null)) - .waitForTimelineChanged( - adErrorTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(adErrorTimeline) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(fakeMediaSource) + .setMediaSource(fakeMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -571,31 +543,26 @@ public final class ExoPlayerTest { } @Test - public void testIllegalSeekPositionDoesThrow() throws Exception { - final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; + public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception { ActionSchedule actionSchedule = - new ActionSchedule.Builder("testIllegalSeekPositionDoesThrow") + new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition") .waitForPlaybackState(Player.STATE_BUFFERING) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - try { - player.seekTo(/* windowIndex= */ 100, /* positionMs= */ 0); - } catch (IllegalSeekPositionException e) { - exception[0] = e; - } - } - }) + // The illegal seek position will end playback. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) .waitForPlaybackState(Player.STATE_ENDED) .build(); - new Builder() - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - assertThat(exception[0]).isNotNull(); + final boolean[] onSeekProcessedCalled = new boolean[1]; + EventListener listener = + new EventListener() { + @Override + public void onSeekProcessed() { + onSeekProcessedCalled[0] = true; + } + }; + ExoPlayerTestRunner testRunner = + new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context); + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + assertThat(onSeekProcessedCalled[0]).isTrue(); } @Test @@ -639,7 +606,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -667,7 +634,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -693,7 +660,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -711,7 +678,7 @@ public final class ExoPlayerTest { FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -740,7 +707,7 @@ public final class ExoPlayerTest { FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -777,7 +744,7 @@ public final class ExoPlayerTest { .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -816,7 +783,7 @@ public final class ExoPlayerTest { .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -840,35 +807,31 @@ public final class ExoPlayerTest { @Test public void testDynamicTimelineChangeReason() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline1) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null)) - .waitForTimelineChanged( - timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline2) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline, timeline2); + testRunner.assertTimelinesEqual(timeline1, timeline2); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC); } @Test - public void testResetMediaItemsWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -891,19 +854,17 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_READY) .setShuffleModeEnabled(true) - // Set the second media source (with position reset). + // Reprepare with second media source (keeping state, but with position reset). // Plays period 1 and 0 because of the reversed fake shuffle order. - .setMediaItems(/* resetPosition= */ true, secondMediaSource) + .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) .play() - .waitForPositionDiscontinuity() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(firstMediaSource) + .setMediaSource(firstMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0); } @@ -948,7 +909,7 @@ public final class ExoPlayerTest { .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -981,10 +942,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @@ -1015,10 +974,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isAtLeast(50L); } @@ -1049,11 +1006,9 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET); testRunner.assertNoPositionDiscontinuities(); assertThat(positionHolder[0]).isEqualTo(0); } @@ -1099,29 +1054,15 @@ public final class ExoPlayerTest { } @Test - public void testSettingNewStartPositionPossibleAfterStopWithReset() throws Exception { + public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); - AtomicInteger windowIndexAfterStop = new AtomicInteger(); - AtomicLong positionAfterStop = new AtomicLong(); + MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSettingNewStartPositionPossibleAfterStopWithReset") + new ActionSchedule.Builder("testRepreparationAfterStop") .waitForPlaybackState(Player.STATE_READY) .stop(/* reset= */ true) .waitForPlaybackState(Player.STATE_IDLE) - .seek(/* windowIndex= */ 1, /* positionMs= */ 1000) - .setMediaItems(secondSource) - .prepare() - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - windowIndexAfterStop.set(player.getCurrentWindowIndex()); - positionAfterStop.set(player.getCurrentPosition()); - } - }) - .waitForPlaybackState(Player.STATE_READY) + .prepareSource(secondSource) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -1131,51 +1072,62 @@ public final class ExoPlayerTest { .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_IDLE, - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_ENDED); - testRunner.assertTimelinesSame( - dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, secondTimeline); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // stop(true) - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - assertThat(windowIndexAfterStop.get()).isEqualTo(1); - assertThat(positionAfterStop.get()).isAtLeast(1000L); + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + @Test + public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekAfterStopWithReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + // If we were still using the first timeline, this would throw. + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); testRunner.assertPlayedPeriodIndices(0, 1); } @Test - public void testResetPlaylistWithPreviousPosition() throws Exception { - Object firstWindowId = new Object(); + public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception { Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); - Timeline firstExpectedMaskingTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); - Object secondWindowId = new Object(); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); Timeline secondTimeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); - Timeline secondExpectedMaskingTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); MediaSource secondSource = new FakeMediaSource(secondTimeline); AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = - new ActionSchedule.Builder("testResetPlaylistWithPreviousPosition") + new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .setMediaItems(/* windowIndex= */ 0, /* positionMs= */ 2000, secondSource) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .waitForTimelineChanged(secondTimeline) .executeRunnable( new PlayerRunnable() { @Override @@ -1194,68 +1146,10 @@ public final class ExoPlayerTest { .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame( - firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); assertThat(positionAfterReprepare.get()).isAtLeast(2000L); } - @Test - public void testResetPlaylistStartsFromDefaultPosition() throws Exception { - Object firstWindowId = new Object(); - Timeline timeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); - Timeline firstExpectedDummyTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); - Object secondWindowId = new Object(); - Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); - Timeline secondExpectedDummyTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); - MediaSource secondSource = new FakeMediaSource(secondTimeline); - AtomicLong positionAfterReprepare = new AtomicLong(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testResetPlaylistStartsFromDefaultPosition") - .pause() - .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .setMediaItems(secondSource) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - positionAfterReprepare.set(player.getCurrentPosition()); - } - }) - .play() - .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - testRunner.assertTimelinesSame( - firstExpectedDummyTimeline, timeline, secondExpectedDummyTimeline, secondTimeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - assertThat(positionAfterReprepare.get()).isEqualTo(0L); - } - @Test public void testStopDuringPreparationOverwritesPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); @@ -1274,10 +1168,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, Timeline.EMPTY); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + testRunner.assertTimelinesEqual(Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @@ -1302,10 +1194,8 @@ public final class ExoPlayerTest { .start() .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelinesSame(dummyTimeline, timeline); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } @@ -1317,7 +1207,8 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .prepare() + .prepareSource( + new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = @@ -1331,10 +1222,9 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); } @Test @@ -1356,7 +1246,8 @@ public final class ExoPlayerTest { positionHolder[0] = player.getCurrentPosition(); } }) - .prepare() + .prepareSource( + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @@ -1378,29 +1269,52 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesSame(dummyTimeline, timeline); + testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); assertThat(positionHolder[0]).isEqualTo(50); assertThat(positionHolder[1]).isEqualTo(50); } + @Test + public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception { + final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Seeking to an invalid position will end playback. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(context); + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + } + @Test public void testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + ConcatenatingMediaSource concatenatingMediaSource = + new ConcatenatingMediaSource( + /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); AtomicInteger windowIndexAfterUpdate = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod") - .setShuffleOrder(new FakeShuffleOrder(/* length= */ 0)) .setShuffleModeEnabled(true) .waitForPlaybackState(Player.STATE_BUFFERING) // Seeking to an invalid position will end playback. - .seek( - /* windowIndex= */ 100, /* positionMs= */ 0, /* catchIllegalSeekException= */ true) + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) .waitForPlaybackState(Player.STATE_ENDED) .executeRunnable( new PlayerRunnable() { @@ -1410,13 +1324,12 @@ public final class ExoPlayerTest { } }) .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context); + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); assertThat(windowIndexAfterUpdate.get()).isEqualTo(1); } @@ -1450,7 +1363,7 @@ public final class ExoPlayerTest { }) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1464,7 +1377,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 firstMediaSource = new FakeMediaSource(timeline); + final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1481,7 +1394,8 @@ public final class ExoPlayerTest { windowIndexHolder[0] = player.getCurrentWindowIndex(); } }) - .prepare() + .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_BUFFERING) .executeRunnable( new PlayerRunnable() { @Override @@ -1489,6 +1403,7 @@ public final class ExoPlayerTest { // Position while repreparing. positionHolder[1] = player.getCurrentPosition(); windowIndexHolder[1] = player.getCurrentWindowIndex(); + secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null); } }) .waitForPlaybackState(Player.STATE_READY) @@ -1505,7 +1420,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(firstMediaSource) + .setTimeline(timeline) .setActionSchedule(actionSchedule) .build(context); try { @@ -1532,8 +1447,7 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .seek(0, C.TIME_UNSET) - .prepare() + .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .play() .build(); @@ -1550,7 +1464,7 @@ public final class ExoPlayerTest { }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .setAnalyticsListener(listener) .build(context); @@ -1566,15 +1480,14 @@ public final class ExoPlayerTest { @Test public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource2 = new FakeMediaSource(timeline); + final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) - .setMediaItems(/* resetPosition= */ false, mediaSource2) - .prepare() + .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_BUFFERING) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) @@ -1590,12 +1503,9 @@ public final class ExoPlayerTest { } catch (ExoPlaybackException e) { // Expected exception. } - testRunner.assertTimelinesSame(dummyTimeline, timeline, dummyTimeline, timeline); + testRunner.assertTimelinesEqual(timeline, timeline); testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); } @Test @@ -1625,8 +1535,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* positionMs= */ 50) .play() .build(); @@ -1720,12 +1629,17 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .pause() .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) .play() + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ true) + .waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_ENDED) .build(); new Builder() @@ -1772,8 +1686,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .seek(/* positionMs= */ 50) .build(); new Builder() @@ -1814,8 +1727,7 @@ public final class ExoPlayerTest { new ActionSchedule.Builder("testSendMessages") .pause() .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .seek(/* positionMs= */ 51) .play() .build(); @@ -1894,16 +1806,14 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* positionMs= */ 50) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(secondTimeline) .play() .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1919,7 +1829,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForPlaybackState(Player.STATE_BUFFERING) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); @@ -1940,8 +1850,7 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) .play() .build(); @@ -1970,17 +1879,15 @@ public final class ExoPlayerTest { ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") .pause() - .waitForTimelineChanged( - timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline) .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null)) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(secondTimeline) .seek(/* windowIndex= */ 0, /* positionMs= */ 0) .play() .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2014,7 +1921,7 @@ public final class ExoPlayerTest { .play() .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2152,21 +2059,16 @@ public final class ExoPlayerTest { /* windowIndex= */ 0, /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null)) - .waitForTimelineChanged( - timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .waitForTimelineChanged(timeline2) .play() .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); - testRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); testRunner.assertPlayedPeriodIndices(0, 1); // Assert that the second period was re-created from the new timeline. assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3); @@ -2208,7 +2110,7 @@ public final class ExoPlayerTest { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2326,56 +2228,6 @@ public final class ExoPlayerTest { assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder(); } - @Test - public void testRecursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception { - Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 3); - final AtomicReference playerReference = new AtomicReference<>(); - FakeMediaSource secondMediaSource = new FakeMediaSource(secondTimeline); - final EventListener eventListener = - new EventListener() { - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - if (state == Player.STATE_IDLE) { - playerReference.get().setMediaItem(secondMediaSource); - } - } - }; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRecursiveTimelineChangeInStopAreReportedInCorrectOrder") - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - playerReference.set(player); - player.addListener(eventListener); - } - }) - .waitForTimelineChanged(firstTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - // Ensure there are no further pending callbacks. - .delay(1) - .stop(/* reset= */ true) - .prepare() - .waitForTimelineChanged(secondTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setActionSchedule(actionSchedule) - .setTimeline(firstTimeline) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, firstTimeline, Timeline.EMPTY, dummyTimeline, secondTimeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - } - @Test public void testClippedLoopedPeriodsArePlayedFully() throws Exception { long startPositionUs = 300_000; @@ -2428,7 +2280,7 @@ public final class ExoPlayerTest { .build(); new ExoPlayerTestRunner.Builder() .setClock(clock) - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2462,7 +2314,7 @@ public final class ExoPlayerTest { List trackGroupsList = new ArrayList<>(); List trackSelectionsList = new ArrayList<>(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .setActionSchedule(actionSchedule) .setEventListener( @@ -2510,7 +2362,7 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setRenderers(renderer) .build(context); try { @@ -2553,7 +2405,49 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) + .setActionSchedule(actionSchedule) + .setRenderers(renderer) + .build(context); + try { + testRunner.start().blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertThat(renderer.sampleBufferReadCount).isAtLeast(1); + assertThat(renderer.hasReadStreamToEnd()).isTrue(); + } + + @Test + public void failingDynamicUpdateOnlyThrowsWhenAvailablePeriodHasBeenFullyRead() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ 10 * C.MICROS_PER_SECOND)); + AtomicReference wasReadyOnce = new AtomicReference<>(false); + MediaSource mediaSource = + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) { + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + if (wasReadyOnce.get()) { + throw new IOException(); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testFailingDynamicMediaSourceInTimelineOnlyThrowsLater") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(() -> wasReadyOnce.set(true)) + .play() + .build(); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = + new Builder() + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .setRenderers(renderer) .build(context); @@ -2587,7 +2481,7 @@ public final class ExoPlayerTest { .executeRunnable(concatenatingMediaSource::clear) .build(); new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2611,7 +2505,7 @@ public final class ExoPlayerTest { .pause() .waitForPlaybackState(Player.STATE_BUFFERING) .seek(/* positionMs= */ 10) - .waitForSeekProcessed() + .waitForTimelineChanged() .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) @@ -2625,7 +2519,7 @@ public final class ExoPlayerTest { .play() .build(); new Builder() - .setMediaSources(concatenatedMediaSource) + .setMediaSource(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2656,7 +2550,7 @@ public final class ExoPlayerTest { .waitForPlaybackState(Player.STATE_BUFFERING) // Seek 10ms into the second period. .seek(/* positionMs= */ periodDurationMs + 10) - .waitForSeekProcessed() + .waitForTimelineChanged() .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null)) .waitForTimelineChanged() .waitForPlaybackState(Player.STATE_READY) @@ -2671,7 +2565,7 @@ public final class ExoPlayerTest { .play() .build(); new Builder() - .setMediaSources(concatenatedMediaSource) + .setMediaSource(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2772,10 +2666,10 @@ public final class ExoPlayerTest { player.addListener(eventListener); } }) - .seek(/* positionMs= */ 5_000) + .seek(5_000) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(fakeMediaSource) + .setMediaSource(fakeMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2873,506 +2767,6 @@ public final class ExoPlayerTest { .inOrder(); } - @Test - public void testMoveMediaItem() throws Exception { - TimelineWindowDefinition firstWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition secondWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - Timeline timeline1 = new FakeTimeline(firstWindowDefinition); - Timeline timeline2 = new FakeTimeline(secondWindowDefinition); - MediaSource mediaSource1 = new FakeMediaSource(timeline1); - MediaSource mediaSource2 = new FakeMediaSource(timeline2); - Timeline expectedDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testMoveMediaItem") - .waitForTimelineChanged( - /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource1, mediaSource2) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - Timeline expectedRealTimeline = new FakeTimeline(firstWindowDefinition, secondWindowDefinition); - Timeline expectedRealTimelineAfterMove = - new FakeTimeline(secondWindowDefinition, firstWindowDefinition); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - exoPlayerTestRunner.assertTimelinesSame( - expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterMove); - } - - @Test - public void testRemoveMediaItem() throws Exception { - TimelineWindowDefinition firstWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition secondWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition thirdWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - Timeline timeline1 = new FakeTimeline(firstWindowDefinition); - Timeline timeline2 = new FakeTimeline(secondWindowDefinition); - Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); - MediaSource mediaSource1 = new FakeMediaSource(timeline1); - MediaSource mediaSource2 = new FakeMediaSource(timeline2); - MediaSource mediaSource3 = new FakeMediaSource(timeline3); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRemoveMediaItems") - .waitForPlaybackState(Player.STATE_READY) - .removeMediaItem(/* index= */ 0) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource1, mediaSource2, mediaSource3) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - Timeline expectedDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - Timeline expectedRealTimeline = - new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); - Timeline expectedRealTimelineAfterRemove = - new FakeTimeline(secondWindowDefinition, thirdWindowDefinition); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - exoPlayerTestRunner.assertTimelinesSame( - expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); - } - - @Test - public void testRemoveMediaItems() throws Exception { - TimelineWindowDefinition firstWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition secondWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - TimelineWindowDefinition thirdWindowDefinition = - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ C.msToUs(10000)); - Timeline timeline1 = new FakeTimeline(firstWindowDefinition); - Timeline timeline2 = new FakeTimeline(secondWindowDefinition); - Timeline timeline3 = new FakeTimeline(thirdWindowDefinition); - MediaSource mediaSource1 = new FakeMediaSource(timeline1); - MediaSource mediaSource2 = new FakeMediaSource(timeline2); - MediaSource mediaSource3 = new FakeMediaSource(timeline3); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testRemoveMediaItems") - .waitForPlaybackState(Player.STATE_READY) - .removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource1, mediaSource2, mediaSource3) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - Timeline expectedDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 1, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 2, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 3, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - Timeline expectedRealTimeline = - new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition); - Timeline expectedRealTimelineAfterRemove = new FakeTimeline(firstWindowDefinition); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); - exoPlayerTestRunner.assertTimelinesSame( - expectedDummyTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove); - } - - @Test - public void testClearMediaItems() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testClearMediaItems") - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .waitForPlaybackState(Player.STATE_READY) - .clearMediaItems() - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline, Timeline.EMPTY); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */); - } - - @Test - public void testMultipleModificationWithRecursiveListenerInvocations() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource mediaSource = new FakeMediaSource(timeline); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testMultipleModificationWithRecursiveListenerInvocations") - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .clearMediaItems() - .setMediaItems(secondMediaSource) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(mediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, secondTimeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); - } - - @Test - public void testModifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering() - throws Exception { - Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource firstMediaSource = new FakeMediaSource(firstTimeline); - Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource secondMediaSource = new FakeMediaSource(secondTimeline); - int[] playbackStates = new int[4]; - int[] timelineWindowCounts = new int[4]; - ActionSchedule actionSchedule = - new ActionSchedule.Builder( - "testModifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering") - .waitForTimelineChanged(dummyTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 0, playbackStates, timelineWindowCounts)) - .clearMediaItems() - .executeRunnable( - new PlaybackStateCollector(/* index= */ 1, playbackStates, timelineWindowCounts)) - .setMediaItems(/* windowIndex= */ 0, /* positionMs= */ 1000, firstMediaSource) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 2, playbackStates, timelineWindowCounts)) - .addMediaItems(secondMediaSource) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts)) - .seek(/* windowIndex= */ 1, /* positionMs= */ 2000) - .waitForSeekProcessed() - .prepare() - // The first expected buffering state arrives after prepare but not before. - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSources(firstMediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start(/* doPrepare= */ false) - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertArrayEquals( - new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, - playbackStates); - assertArrayEquals(new int[] {1, 0, 1, 2}, timelineWindowCounts); - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING /* first buffering state after prepare */, - Player.STATE_READY, - Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* initial setMediaItems */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* set media items */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* add media items */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source update after prepare */); - Timeline expectedSecondDummyTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ false, - /* isDynamic= */ true, - /* durationUs= */ C.TIME_UNSET)); - Timeline expectedSecondRealTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ 10_000_000), - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false, - /* durationUs= */ 10_000_000)); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, - Timeline.EMPTY, - dummyTimeline, - expectedSecondDummyTimeline, - expectedSecondRealTimeline); - } - - @Test - public void testModifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); - ActionSchedule actionSchedule = - new ActionSchedule.Builder( - "testModifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering") - .waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .clearMediaItems() - .waitForPlaybackState(Player.STATE_ENDED) - .addMediaItems(secondMediaSource) // add must not transition to buffering - .waitForTimelineChanged() - .clearMediaItems() // clear must remain in ended - .addMediaItems(secondMediaSource) // add again to be able to test the seek - .waitForTimelineChanged() - .seek(/* positionMs= */ 2_000) // seek must transition to buffering - .waitForSeekProcessed() - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING, // first buffering - Player.STATE_READY, - Player.STATE_ENDED, // clear playlist - Player.STATE_BUFFERING, // second buffering after seek - Player.STATE_READY, - Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, - timeline, - Timeline.EMPTY, - dummyTimeline, - timeline, - Timeline.EMPTY, - dummyTimeline, - timeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); - } - - @Test - public void testStopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering() - throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource secondMediaSource = new FakeMediaSource(timeline); - int[] playbackStateHolder = new int[3]; - int[] windowCountHolder = new int[3]; - ActionSchedule actionSchedule = - new ActionSchedule.Builder( - "testStopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering") - .waitForPlaybackState(Player.STATE_READY) - .stop(/* reset= */ false) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 0, playbackStateHolder, windowCountHolder)) - .clearMediaItems() - .executeRunnable( - new PlaybackStateCollector(/* index= */ 1, playbackStateHolder, windowCountHolder)) - .addMediaItems(secondMediaSource) - .executeRunnable( - new PlaybackStateCollector(/* index= */ 2, playbackStateHolder, windowCountHolder)) - .prepare() - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - assertArrayEquals( - new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, playbackStateHolder); - assertArrayEquals(new int[] {1, 0, 1}, windowCountHolder); - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, - Player.STATE_BUFFERING, // first buffering - Player.STATE_READY, - Player.STATE_IDLE, // stop - Player.STATE_BUFFERING, - Player.STATE_READY, - Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame( - dummyTimeline, timeline, Timeline.EMPTY, dummyTimeline, timeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, /* source prepared */ - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear media items */, - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item add (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); - } - - @Test - public void testPrepareWhenAlreadyPreparedIsANoop() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testPrepareWhenAlreadyPreparedIsANoop") - .waitForPlaybackState(Player.STATE_READY) - .prepare() - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - - exoPlayerTestRunner.assertPlaybackStatesEqual( - Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED); - exoPlayerTestRunner.assertTimelinesSame(dummyTimeline, timeline); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual( - Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */, - Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); - } - // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { 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 b137cd3cff..afcce904e9 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 @@ -21,17 +21,15 @@ import static org.mockito.Mockito.mock; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.ShuffleOrder; 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.testutil.FakeMediaSource; 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 java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,20 +49,19 @@ public final class MediaPeriodQueueTest { private MediaPeriodQueue mediaPeriodQueue; private AdPlaybackState adPlaybackState; + private Timeline timeline; private Object periodUid; private PlaybackInfo playbackInfo; private RendererCapabilities[] rendererCapabilities; private TrackSelector trackSelector; private Allocator allocator; - private Playlist playlist; - private FakeMediaSource fakeMediaSource; - private Playlist.MediaSourceHolder mediaSourceHolder; + private MediaSource mediaSource; @Before public void setUp() { mediaPeriodQueue = new MediaPeriodQueue(); - playlist = mock(Playlist.class); + mediaSource = mock(MediaSource.class); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); @@ -72,7 +69,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupTimeline(); + setupTimeline(/* initialPositionUs= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, @@ -83,7 +80,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* adGroupTimesUs= */ 0); + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); advance(); @@ -97,7 +94,10 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, @@ -132,7 +132,10 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, @@ -165,7 +168,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { - setupTimeline(/* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_END_OF_SOURCE, @@ -185,7 +188,10 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -195,8 +201,10 @@ public final class MediaPeriodQueueTest { enqueueNext(); // Second ad. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US + 1); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -210,7 +218,10 @@ public final class MediaPeriodQueueTest { @Test public void updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { - setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); enqueueNext(); // Content before first ad. @@ -221,8 +232,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading first ad. // Change position of first ad (= change duration of content before first ad). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, + SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -237,6 +250,7 @@ public final class MediaPeriodQueueTest { public void updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { setupTimeline( + /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -250,8 +264,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; @@ -268,6 +284,7 @@ public final class MediaPeriodQueueTest { public void updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { setupTimeline( + /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -281,8 +298,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; @@ -299,6 +318,7 @@ public final class MediaPeriodQueueTest { public void updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { setupTimeline( + /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); setAdGroupLoaded(/* adGroupIndex= */ 0); @@ -312,8 +332,10 @@ public final class MediaPeriodQueueTest { advanceReading(); // Reading content between ads. // Change position of second ad (= change duration of content between ads). - updateAdPlaybackStateAndTimeline( - /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 1); boolean changeHandled = @@ -324,25 +346,16 @@ public final class MediaPeriodQueueTest { assertThat(getQueueLength()).isEqualTo(3); } - private void setupTimeline(long... adGroupTimesUs) { + private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); - - // Create a media source holder. - SinglePeriodAdTimeline adTimeline = - new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - fakeMediaSource = new FakeMediaSource(adTimeline); - mediaSourceHolder = new Playlist.MediaSourceHolder(fakeMediaSource, false); - mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null); - - Timeline timeline = createPlaylistTimeline(); + timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); mediaPeriodQueue.setTimeline(timeline); - playbackInfo = new PlaybackInfo( timeline, - mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0), + mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, Player.STATE_READY, @@ -356,25 +369,6 @@ public final class MediaPeriodQueueTest { /* positionUs= */ 0); } - private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) { - adPlaybackState = - new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); - updateTimeline(); - } - - private void updateTimeline() { - SinglePeriodAdTimeline adTimeline = - new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null); - mediaPeriodQueue.setTimeline(createPlaylistTimeline()); - } - - private Playlist.PlaylistTimeline createPlaylistTimeline() { - return new Playlist.PlaylistTimeline( - Collections.singleton(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - } - private void advance() { enqueueNext(); if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { @@ -395,7 +389,7 @@ public final class MediaPeriodQueueTest { rendererCapabilities, trackSelector, allocator, - playlist, + mediaSource, getNextMediaPeriodInfo(), new TrackSelectorResult( new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); @@ -427,6 +421,11 @@ public final class MediaPeriodQueueTest { updateTimeline(); } + private void updateTimeline() { + timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + mediaPeriodQueue.setTimeline(timeline); + } + private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( long startPositionUs, long endPositionUs, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java deleted file mode 100644 index cc551db8ac..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java +++ /dev/null @@ -1,510 +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; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeMediaSource; -import com.google.android.exoplayer2.testutil.FakeShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeTimeline; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link Playlist}. */ -@RunWith(AndroidJUnit4.class) -public class PlaylistTest { - - private static final int PLAYLIST_SIZE = 4; - - private Playlist playlist; - - @Before - public void setUp() { - playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class)); - } - - @Test - public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); - List fakeHolders = createFakeHolders(); - - Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); - assertNotSame(timeline, Timeline.EMPTY); - - // Remove all media sources. - timeline = - playlist.removeMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder); - assertSame(timeline, Timeline.EMPTY); - - timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); - assertNotSame(timeline, Timeline.EMPTY); - // Clear. - timeline = playlist.clear(shuffleOrder); - assertSame(timeline, Timeline.EMPTY); - } - - @Test - public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.setMediaSources( - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); - // Verify prepare is called once on prepare. - verify(mockMediaSource1, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - playlist.prepare(/* mediaTransferListener= */ null); - assertThat(playlist.isPrepared()).isTrue(); - // Verify prepare is called once on prepare. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - playlist.release(); - playlist.prepare(/* mediaTransferListener= */ null); - // Verify prepare is called a second time on re-prepare. - verify(mockMediaSource1, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder); - - assertThat(timeline.getWindowCount()).isEqualTo(2); - assertThat(playlist.getSize()).isEqualTo(2); - - // Assert holder offsets have been set properly - for (int i = 0; i < mediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); - } - - // Set media items again. The second holder is re-used. - List moreMediaSources = - createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); - moreMediaSources.add(mediaSources.get(1)); - timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - assertThat(timeline.getWindowCount()).isEqualTo(2); - for (int i = 0; i < moreMediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); - } - // Expect removed holders and sources to be removed without releasing. - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(0).isRemoved).isTrue(); - // Expect re-used holder and source not to be removed. - verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(1).isRemoved).isFalse(); - } - - @Test - public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources(mediaSources, shuffleOrder); - - // Verify sources are prepared. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - // Set media items again. The second holder is re-used. - List moreMediaSources = - createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); - moreMediaSources.add(mediaSources.get(1)); - playlist.setMediaSources(moreMediaSources, shuffleOrder); - - // Expect removed holders and sources to be removed and released. - verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(0).isRemoved).isTrue(); - // Expect re-used holder and source not to be removed but released. - verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(1).isRemoved).isFalse(); - verify(mockMediaSource2, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2)); - - assertThat(playlist.getSize()).isEqualTo(2); - // Verify lazy initialization does not call prepare on sources. - verify(mockMediaSource1, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - for (int i = 0; i < mediaSources.size(); i++) { - assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i); - assertThat(mediaSources.get(i).isRemoved).isFalse(); - } - - // Add for more sources in between. - List moreMediaSources = createFakeHolders(); - playlist.addMediaSources( - /* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3)); - - assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0); - assertThat(moreMediaSources.get(0).firstWindowIndexInChild).isEqualTo(1); - assertThat(moreMediaSources.get(3).firstWindowIndexInChild).isEqualTo(4); - assertThat(mediaSources.get(1).firstWindowIndexInChild).isEqualTo(5); - } - - @Test - public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.prepare(/* mediaTransferListener= */ null); - playlist.addMediaSources( - /* index= */ 0, - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); - - // Verify prepare is called on sources when added. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testMoveMediaSources() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - List holders = createFakeHolders(); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - - assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder); - assertFirstWindowInChildIndices(holders, 3, 0, 1, 2); - playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); - assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( - /* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); - assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( - /* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder); - assertFirstWindowInChildIndices(holders, 0, 3, 1, 2); - playlist.moveMediaSourceRange( - /* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - // No-ops. - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - } - - @Test - public void testRemoveMediaSources_whenUnprepared_expectNoRelease() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - MediaSource mockMediaSource3 = mock(MediaSource.class); - MediaSource mockMediaSource4 = mock(MediaSource.class); - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, - mockMediaSource1, - mockMediaSource2, - mockMediaSource3, - mockMediaSource4); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - Playlist.MediaSourceHolder removedHolder1 = holders.remove(1); - Playlist.MediaSourceHolder removedHolder2 = holders.remove(1); - - assertDefaultFirstWindowInChildIndexOrder(holders); - assertThat(removedHolder1.isRemoved).isTrue(); - assertThat(removedHolder2.isRemoved).isTrue(); - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource3, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - } - - @Test - public void testRemoveMediaSources_whenPrepared_expectRelease() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - MediaSource mockMediaSource3 = mock(MediaSource.class); - MediaSource mockMediaSource4 = mock(MediaSource.class); - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, - mockMediaSource1, - mockMediaSource2, - mockMediaSource3, - mockMediaSource4); - playlist.prepare(/* mediaTransferListener */ null); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - holders.remove(2); - holders.remove(1); - - assertDefaultFirstWindowInChildIndexOrder(holders); - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource3, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - } - - @Test - public void testRelease_playlistUnprepared_expectSourcesNotReleased() { - MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - - playlist.setMediaSources( - Collections.singletonList(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - verify(mockMediaSource, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - } - - @Test - public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() { - MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources( - Collections.singletonList(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - verify(mockMediaSource, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - } - - @Test - public void testClearPlaylist_expectSourcesReleasedAndRemoved() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.setMediaSources(holders, shuffleOrder); - playlist.prepare(/* mediaTransferListener= */ null); - - Timeline timeline = playlist.clear(shuffleOrder); - assertThat(timeline.isEmpty()).isTrue(); - assertThat(holders.get(0).isRemoved).isTrue(); - assertThat(holders.get(1).isRemoved).isTrue(); - verify(mockMediaSource1, times(1)).releaseSource(any()); - verify(mockMediaSource2, times(1)).releaseSource(any()); - } - - @Test - public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() { - Timeline timeline = - playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() { - Timeline timeline = - playlist.addMediaSources( - /* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.moveMediaSource( - /* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, - /* toIndex= */ 2, - /* newFromIndex= */ 2, - new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.removeMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() { - playlist.setMediaSources( - createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder( - playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE))); - } - - // Internal methods. - - private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) { - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(-1); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ timeline.getWindowCount() - 1, - Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(-1); - } - - private static void assertDefaultFirstWindowInChildIndexOrder( - List holders) { - int[] indices = new int[holders.size()]; - for (int i = 0; i < indices.length; i++) { - indices[i] = i; - } - assertFirstWindowInChildIndices(holders, indices); - } - - private static void assertFirstWindowInChildIndices( - List holders, int... firstWindowInChildIndices) { - assertThat(holders).hasSize(firstWindowInChildIndices.length); - for (int i = 0; i < holders.size(); i++) { - assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]); - } - } - - private static List createFakeHolders() { - MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1)); - List holders = new ArrayList<>(); - for (int i = 0; i < PLAYLIST_SIZE; i++) { - holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true)); - } - return holders; - } - - private static List createFakeHoldersWithSources( - boolean useLazyPreparation, MediaSource... sources) { - List holders = new ArrayList<>(); - for (MediaSource mediaSource : sources) { - holders.add( - new Playlist.MediaSourceHolder( - mediaSource, /* useLazyPreparation= */ useLazyPreparation)); - } - return holders; - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index ba05af385a..d6e65cb34d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2; -import static com.google.common.truth.Truth.assertThat; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; @@ -60,142 +58,4 @@ public class TimelineTest { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } - - @Test - public void testWindowEquals() { - Timeline.Window window = new Timeline.Window(); - assertThat(window).isEqualTo(new Timeline.Window()); - - Timeline.Window otherWindow = new Timeline.Window(); - otherWindow.tag = new Object(); - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.manifest = new Object(); - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.presentationStartTimeMs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.windowStartTimeMs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isSeekable = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isDynamic = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.defaultPositionUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.durationUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.firstPeriodIndex = 1; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.lastPeriodIndex = 1; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.positionInFirstPeriodUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - window.uid = new Object(); - window.tag = new Object(); - window.manifest = new Object(); - window.presentationStartTimeMs = C.TIME_UNSET; - window.windowStartTimeMs = C.TIME_UNSET; - window.isSeekable = true; - window.isDynamic = true; - window.defaultPositionUs = C.TIME_UNSET; - window.durationUs = C.TIME_UNSET; - window.firstPeriodIndex = 1; - window.lastPeriodIndex = 1; - window.positionInFirstPeriodUs = C.TIME_UNSET; - otherWindow = - otherWindow.set( - window.uid, - window.tag, - window.manifest, - window.presentationStartTimeMs, - window.windowStartTimeMs, - window.isSeekable, - window.isDynamic, - window.defaultPositionUs, - window.durationUs, - window.firstPeriodIndex, - window.lastPeriodIndex, - window.positionInFirstPeriodUs); - assertThat(window).isEqualTo(otherWindow); - } - - @Test - public void testWindowHashCode() { - Timeline.Window window = new Timeline.Window(); - Timeline.Window otherWindow = new Timeline.Window(); - assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - - window.tag = new Object(); - assertThat(window.hashCode()).isNotEqualTo(otherWindow.hashCode()); - otherWindow.tag = window.tag; - assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - } - - @Test - public void testPeriodEquals() { - Timeline.Period period = new Timeline.Period(); - assertThat(period).isEqualTo(new Timeline.Period()); - - Timeline.Period otherPeriod = new Timeline.Period(); - otherPeriod.id = new Object(); - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.uid = new Object(); - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.windowIndex = 12; - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.durationUs = 11L; - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - period.id = new Object(); - period.uid = new Object(); - period.windowIndex = 1; - period.durationUs = 123L; - otherPeriod = - otherPeriod.set( - period.id, - period.uid, - period.windowIndex, - period.durationUs, - /* positionInWindowUs= */ 0); - assertThat(period).isEqualTo(otherPeriod); - } - - @Test - public void testPeriodHashCode() { - Timeline.Period period = new Timeline.Period(); - Timeline.Period otherPeriod = new Timeline.Period(); - assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); - - period.windowIndex = 12; - assertThat(period.hashCode()).isNotEqualTo(otherPeriod.hashCode()); - otherPeriod.windowIndex = period.windowIndex; - assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); - } } 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 7117f426f3..fb3e0936ae 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 @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -132,29 +133,24 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); listener.assertNoMoreEvents(); } @Test public void testSinglePeriod() throws Exception { FakeMediaSource mediaSource = - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); - populateEventIds(listener.lastReportedTimeline); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); @@ -183,14 +179,9 @@ public final class AnalyticsCollectorTest { public void testAutomaticPeriodTransition() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT), - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -200,8 +191,7 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* BUFFERING */, period0 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -243,8 +233,8 @@ public final class AnalyticsCollectorTest { public void testPeriodTransitionWithRendererChange() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.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); @@ -256,8 +246,7 @@ public final class AnalyticsCollectorTest { period1 /* BUFFERING */, period1 /* READY */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -297,8 +286,8 @@ public final class AnalyticsCollectorTest { public void testSeekToOtherPeriod() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.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() @@ -319,8 +308,7 @@ public final class AnalyticsCollectorTest { period1 /* READY */, period1 /* setPlayWhenReady=true */, period1 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); @@ -362,11 +350,9 @@ public final class AnalyticsCollectorTest { public void testSeekBackAfterReadingAhead() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - ExoPlayerTestRunner.Builder.VIDEO_FORMAT, - ExoPlayerTestRunner.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 = @@ -394,8 +380,7 @@ public final class AnalyticsCollectorTest { period1Seq2 /* BUFFERING */, period1Seq2 /* READY */, period1Seq2 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); @@ -443,28 +428,18 @@ public final class AnalyticsCollectorTest { @Test public void testPrepareNewSource() throws Exception { - MediaSource mediaSource1 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); - MediaSource mediaSource2 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.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() .waitForPlaybackState(Player.STATE_READY) - .setMediaItems(/* resetPosition= */ false, mediaSource2) + .prepareSource(mediaSource2) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); - // Populate all event ids with last timeline (after second prepare). - populateEventIds(listener.lastReportedTimeline); - // Populate event id of period 0, sequence 0 with timeline of initial preparation. - period0Seq0 = - new EventWindowAndPeriodId( - /* windowIndex= */ 0, - new MediaPeriodId( - listener.reportedTimelines.get(1).getUidOfPeriod(/* periodIndex= */ 0), - /* windowSequenceNumber= */ 0)); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -476,16 +451,12 @@ public final class AnalyticsCollectorTest { period0Seq1 /* READY */, period0Seq1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly( - WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* DYNAMIC */, - WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) .containsExactly( - period0Seq0 /* prepared */, WINDOW_0 /* setMediaItems */, period0Seq1 /* prepared */); + period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( WINDOW_0 /* manifest */, @@ -519,20 +490,19 @@ public final class AnalyticsCollectorTest { @Test public void testReprepareAfterError() throws Exception { - MediaSource mediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.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) - .prepare() + .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_ENDED) .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(listener.lastReportedTimeline); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, @@ -586,7 +556,7 @@ public final class AnalyticsCollectorTest { @Test public void testDynamicTimelineChange() throws Exception { MediaSource childMediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); long periodDurationMs = @@ -618,11 +588,7 @@ public final class AnalyticsCollectorTest { period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* BUFFERING */, period1Seq0 /* ENDED */); - assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly( - WINDOW_0 /* PLAYLIST_CHANGED */, - window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */, - period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly( window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); @@ -676,7 +642,7 @@ public final class AnalyticsCollectorTest { .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); - populateEventIds(listener.lastReportedTimeline); + populateEventIds(SINGLE_PERIOD_TIMELINE); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); } @@ -743,7 +709,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = new TestAnalyticsListener(); try { new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderersFactory(renderersFactory) .setAnalyticsListener(listener) .setActionSchedule(actionSchedule) @@ -765,7 +731,7 @@ public final class AnalyticsCollectorTest { private boolean renderedFirstFrame; public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { - super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + super(Builder.VIDEO_FORMAT); eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); decoderCounters = new DecoderCounters(); } @@ -823,7 +789,7 @@ public final class AnalyticsCollectorTest { private boolean notifiedAudioSessionId; public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { - super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT); + super(Builder.AUDIO_FORMAT); eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); decoderCounters = new DecoderCounters(); } @@ -907,12 +873,10 @@ public final class AnalyticsCollectorTest { public Timeline lastReportedTimeline; - private final List reportedTimelines; private final ArrayList reportedEvents; public TestAnalyticsListener() { reportedEvents = new ArrayList<>(); - reportedTimelines = new ArrayList<>(); lastReportedTimeline = Timeline.EMPTY; } @@ -942,7 +906,6 @@ public final class AnalyticsCollectorTest { @Override public void onTimelineChanged(EventTime eventTime, int reason) { lastReportedTimeline = eventTime.timeline; - reportedTimelines.add(eventTime.timeline); reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); } 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 09539bcb76..af6b91fa23 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 @@ -21,7 +21,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.IllegalSeekPositionException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlayerMessage; @@ -29,7 +28,6 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; @@ -38,8 +36,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Log; -import java.util.Arrays; -import java.util.List; /** Base class for actions to perform during playback tests. */ public abstract class Action { @@ -116,7 +112,6 @@ public abstract class Action { private final Integer windowIndex; private final long positionMs; - private final boolean catchIllegalSeekException; /** * Action calls {@link Player#seekTo(long)}. @@ -128,7 +123,6 @@ public abstract class Action { super(tag, "Seek:" + positionMs); this.windowIndex = null; this.positionMs = positionMs; - catchIllegalSeekException = false; } /** @@ -137,191 +131,24 @@ public abstract class Action { * @param tag A tag to use for logging. * @param windowIndex The window to seek to. * @param positionMs The seek position. - * @param catchIllegalSeekException Whether {@link IllegalSeekPositionException} should be - * silently caught or not. */ - public Seek(String tag, int windowIndex, long positionMs, boolean catchIllegalSeekException) { + public Seek(String tag, int windowIndex, long positionMs) { super(tag, "Seek:" + positionMs); this.windowIndex = windowIndex; this.positionMs = positionMs; - this.catchIllegalSeekException = catchIllegalSeekException; } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - try { - if (windowIndex == null) { - player.seekTo(positionMs); - } else { - player.seekTo(windowIndex, positionMs); - } - } catch (IllegalSeekPositionException e) { - if (!catchIllegalSeekException) { - throw e; - } + if (windowIndex == null) { + player.seekTo(positionMs); + } else { + player.seekTo(windowIndex, positionMs); } } } - /** Calls {@link SimpleExoPlayer#setMediaItems(List, int, long)}. */ - public static final class SetMediaItems extends Action { - - private final int windowIndex; - private final long positionMs; - private final MediaSource[] mediaSources; - - /** - * @param tag A tag to use for logging. - * @param windowIndex The window index to start playback from. - * @param positionMs The position in milliseconds to start playback from. - * @param mediaSources The media sources to populate the playlist with. - */ - public SetMediaItems( - String tag, int windowIndex, long positionMs, MediaSource... mediaSources) { - super(tag, "SetMediaItems"); - this.windowIndex = windowIndex; - this.positionMs = positionMs; - this.mediaSources = mediaSources; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.setMediaItems(Arrays.asList(mediaSources), windowIndex, positionMs); - } - } - - /** Calls {@link SimpleExoPlayer#addMediaItems(List)}. */ - public static final class AddMediaItems extends Action { - - private final MediaSource[] mediaSources; - - /** - * @param tag A tag to use for logging. - * @param mediaSources The media sources to be added to the playlist. - */ - public AddMediaItems(String tag, MediaSource... mediaSources) { - super(tag, /* description= */ "AddMediaItems"); - this.mediaSources = mediaSources; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.addMediaItems(Arrays.asList(mediaSources)); - } - } - - /** Calls {@link SimpleExoPlayer#setMediaItems(List, boolean)}. */ - public static final class SetMediaItemsResetPosition extends Action { - - private final boolean resetPosition; - private final MediaSource[] mediaSources; - - /** - * @param tag A tag to use for logging. - * @param resetPosition Whether the position should be reset. - * @param mediaSources The media sources to populate the playlist with. - */ - public SetMediaItemsResetPosition( - String tag, boolean resetPosition, MediaSource... mediaSources) { - super(tag, "SetMediaItems"); - this.resetPosition = resetPosition; - this.mediaSources = mediaSources; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.setMediaItems(Arrays.asList(mediaSources), resetPosition); - } - } - - /** Calls {@link SimpleExoPlayer#moveMediaItem(int, int)}. */ - public static class MoveMediaItem extends Action { - - private final int currentIndex; - private final int newIndex; - - /** - * @param tag A tag to use for logging. - * @param currentIndex The current index of the media item. - * @param newIndex The new index of the media item. - */ - public MoveMediaItem(String tag, int currentIndex, int newIndex) { - super(tag, "MoveMediaItem"); - this.currentIndex = currentIndex; - this.newIndex = newIndex; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.moveMediaItem(currentIndex, newIndex); - } - } - - /** Calls {@link SimpleExoPlayer#removeMediaItem(int)}. */ - public static class RemoveMediaItem extends Action { - - private final int index; - - /** - * @param tag A tag to use for logging. - * @param index The index of the item to remove. - */ - public RemoveMediaItem(String tag, int index) { - super(tag, "RemoveMediaItem"); - this.index = index; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.removeMediaItem(index); - } - } - - /** Calls {@link SimpleExoPlayer#removeMediaItems(int, int)}. */ - public static class RemoveMediaItems extends Action { - - private final int fromIndex; - private final int toIndex; - - /** - * @param tag A tag to use for logging. - * @param fromIndex The start if the range of media items to remove. - * @param toIndex The end of the range of media items to remove (exclusive). - */ - public RemoveMediaItems(String tag, int fromIndex, int toIndex) { - super(tag, "RemoveMediaItem"); - this.fromIndex = fromIndex; - this.toIndex = toIndex; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.removeMediaItems(fromIndex, toIndex); - } - } - - /** Calls {@link SimpleExoPlayer#clearMediaItems()}}. */ - public static class ClearMediaItems extends Action { - - /** @param tag A tag to use for logging. */ - public ClearMediaItems(String tag) { - super(tag, "ClearMediaItems"); - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.clearMediaItems(); - } - } - /** Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { @@ -380,6 +207,7 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlayWhenReady(playWhenReady); } + } /** @@ -440,28 +268,42 @@ public abstract class Action { } } - /** Calls {@link ExoPlayer#prepare()}. */ - public static final class Prepare extends Action { + /** Calls {@link ExoPlayer#prepare(MediaSource)}. */ + public static final class PrepareSource extends Action { + + private final MediaSource mediaSource; + private final boolean resetPosition; + private final boolean resetState; + /** @param tag A tag to use for logging. */ - public Prepare(String tag) { - super(tag, "Prepare"); + public PrepareSource(String tag, MediaSource mediaSource) { + this(tag, mediaSource, true, true); + } + + /** @param tag A tag to use for logging. */ + public PrepareSource( + String tag, MediaSource mediaSource, boolean resetPosition, boolean resetState) { + super(tag, "PrepareSource"); + this.mediaSource = mediaSource; + this.resetPosition = resetPosition; + this.resetState = resetState; } @Override protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.prepare(); + player.prepare(mediaSource, resetPosition, resetState); } } /** Calls {@link Player#setRepeatMode(int)}. */ public static final class SetRepeatMode extends Action { - @Player.RepeatMode private final int repeatMode; + private final @Player.RepeatMode int repeatMode; /** @param tag A tag to use for logging. */ public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { - super(tag, "SetRepeatMode: " + repeatMode); + super(tag, "SetRepeatMode:" + repeatMode); this.repeatMode = repeatMode; } @@ -472,27 +314,6 @@ public abstract class Action { } } - /** Calls {@link ExoPlayer#setShuffleOrder(ShuffleOrder)} . */ - public static final class SetShuffleOrder extends Action { - - private final ShuffleOrder shuffleOrder; - - /** - * @param tag A tag to use for logging. - * @param shuffleOrder The shuffle order. - */ - public SetShuffleOrder(String tag, ShuffleOrder shuffleOrder) { - super(tag, "SetShufflerOrder"); - this.shuffleOrder = shuffleOrder; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - player.setShuffleOrder(shuffleOrder); - } - } - /** Calls {@link Player#setShuffleModeEnabled(boolean)}. */ public static final class SetShuffleModeEnabled extends Action { @@ -500,7 +321,7 @@ public abstract class Action { /** @param tag A tag to use for logging. */ public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { - super(tag, "SetShuffleModeEnabled: " + shuffleModeEnabled); + super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); this.shuffleModeEnabled = shuffleModeEnabled; } @@ -587,6 +408,7 @@ public abstract class Action { SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlaybackParameters(playbackParameters); } + } /** Throws a playback exception on the playback thread. */ @@ -683,35 +505,18 @@ public abstract class Action { /** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */ public static final class WaitForTimelineChanged extends Action { - private final Timeline expectedTimeline; - private final boolean ignoreExpectedReason; - @Player.TimelineChangeReason private final int expectedReason; + @Nullable private final Timeline expectedTimeline; /** - * Creates action waiting for a timeline change for a given reason. + * Creates action waiting for a timeline change. * * @param tag A tag to use for logging. - * @param expectedTimeline The expected timeline or null if any timeline change is relevant. - * @param expectedReason The expected timeline change reason. + * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline + * change. */ - public WaitForTimelineChanged( - String tag, Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { + public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) { super(tag, "WaitForTimelineChanged"); this.expectedTimeline = expectedTimeline; - this.ignoreExpectedReason = false; - this.expectedReason = expectedReason; - } - - /** - * Creates action waiting for any timeline change for any reason. - * - * @param tag A tag to use for logging. - */ - public WaitForTimelineChanged(String tag) { - super(tag, "WaitForTimelineChanged"); - this.expectedTimeline = null; - this.ignoreExpectedReason = true; - this.expectedReason = Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } @Override @@ -729,9 +534,7 @@ public abstract class Action { @Override public void onTimelineChanged( Timeline timeline, @Player.TimelineChangeReason int reason) { - if ((expectedTimeline == null - || TestUtil.areTimelinesSame(expectedTimeline, timeline)) - && (ignoreExpectedReason || expectedReason == reason)) { + if (expectedTimeline == null || timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); } @@ -919,7 +722,7 @@ public abstract class Action { } } - /** Calls {@code Runnable.run()}. */ + /** Calls {@link Runnable#run()}. */ public static final class ExecuteRunnable extends Action { private final Runnable runnable; 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 b977956a92..c77e88c981 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 @@ -27,10 +27,10 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition; +import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; -import com.google.android.exoplayer2.testutil.Action.SetShuffleOrder; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; @@ -170,19 +169,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder seek(int windowIndex, long positionMs) { - return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false)); - } - - /** - * Schedules a seek action to be executed. - * - * @param windowIndex The window to seek to. - * @param positionMs The seek position. - * @param catchIllegalSeekException Whether an illegal seek position should be caught or not. - * @return The builder, for convenience. - */ - public Builder seek(int windowIndex, long positionMs, boolean catchIllegalSeekException) { - return apply(new Seek(tag, windowIndex, positionMs, catchIllegalSeekException)); + return apply(new Seek(tag, windowIndex, positionMs)); } /** @@ -314,100 +301,23 @@ public final class ActionSchedule { } /** - * Schedules a set media items action to be executed. + * Schedules a new source preparation action to be executed. * - * @param windowIndex The window index to start playback from or {@link C#INDEX_UNSET} if the - * playback position should not be reset. - * @param positionMs The position in milliseconds from where playback should start. If {@link - * C#TIME_UNSET} is passed the default position is used. In any case, if {@code windowIndex} - * is set to {@link C#INDEX_UNSET} the position is not reset at all and this parameter is - * ignored. * @return The builder, for convenience. */ - public Builder setMediaItems(int windowIndex, long positionMs, MediaSource... sources) { - return apply(new Action.SetMediaItems(tag, windowIndex, positionMs, sources)); + public Builder prepareSource(MediaSource mediaSource) { + return apply(new PrepareSource(tag, mediaSource)); } /** - * Schedules a set media items action to be executed. + * Schedules a new source preparation action to be executed. * - * @param resetPosition Whether the playback position should be reset. - * @return The builder, for convenience. - */ - public Builder setMediaItems(boolean resetPosition, MediaSource... sources) { - return apply(new Action.SetMediaItemsResetPosition(tag, resetPosition, sources)); - } - - /** - * Schedules a set media items action to be executed. - * - * @param mediaSources The media sources to add. - * @return The builder, for convenience. - */ - public Builder setMediaItems(MediaSource... mediaSources) { - return apply( - new Action.SetMediaItems( - tag, /* windowIndex= */ C.INDEX_UNSET, /* positionMs= */ C.TIME_UNSET, mediaSources)); - } - /** - * Schedules a add media items action to be executed. - * - * @param mediaSources The media sources to add. - * @return The builder, for convenience. - */ - public Builder addMediaItems(MediaSource... mediaSources) { - return apply(new Action.AddMediaItems(tag, mediaSources)); - } - - /** - * Schedules a move media item action to be executed. - * - * @param currentIndex The current index of the item to move. - * @param newIndex The index after the item has been moved. - * @return The builder, for convenience. - */ - public Builder moveMediaItem(int currentIndex, int newIndex) { - return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex)); - } - - /** - * Schedules a remove media item action to be executed. - * - * @param index The index of the media item to be removed. * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean) * @return The builder, for convenience. */ - public Builder removeMediaItem(int index) { - return apply(new Action.RemoveMediaItem(tag, index)); - } - - /** - * Schedules a remove media items action to be executed. - * - * @param fromIndex The start of the range of media items to be removed. - * @param toIndex The end of the range of media items to be removed (exclusive). - * @return The builder, for convenience. - */ - public Builder removeMediaItems(int fromIndex, int toIndex) { - return apply(new Action.RemoveMediaItems(tag, fromIndex, toIndex)); - } - - /** - * Schedules a prepare action to be executed. - * - * @return The builder, for convenience. - */ - public Builder prepare() { - return apply(new Action.Prepare(tag)); - } - - /** - * Schedules a clear media items action to be created. - * - * @return The builder. for convenience, - */ - public Builder clearMediaItems() { - return apply(new Action.ClearMediaItems(tag)); + public Builder prepareSource( + MediaSource mediaSource, boolean resetPosition, boolean resetState) { + return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); } /** @@ -419,16 +329,6 @@ public final class ActionSchedule { return apply(new SetRepeatMode(tag, repeatMode)); } - /** - * Schedules a set shuffle order action to be executed. - * - * @param shuffleOrder The shuffle order. - * @return The builder, for convenience. - */ - public Builder setShuffleOrder(ShuffleOrder shuffleOrder) { - return apply(new SetShuffleOrder(tag, shuffleOrder)); - } - /** * Schedules a shuffle setting action to be executed. * @@ -482,19 +382,18 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder waitForTimelineChanged() { - return apply(new WaitForTimelineChanged(tag)); + return apply(new WaitForTimelineChanged(tag, /* expectedTimeline= */ null)); } /** * Schedules a delay until the timeline changed to a specified expected timeline. * - * @param expectedTimeline The expected timeline. - * @param expectedReason The expected reason of the timeline change. + * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline + * change. * @return The builder, for convenience. */ - public Builder waitForTimelineChanged( - Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) { - return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason)); + public Builder waitForTimelineChanged(Timeline expectedTimeline) { + return apply(new WaitForTimelineChanged(tag, expectedTimeline)); } /** 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 b00ad287bb..5f01d7724b 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 @@ -141,8 +141,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { pendingSchedule = null; } DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); - player.setMediaItem(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); - player.prepare(); + player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager)); } @Override 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 f5d322d0ca..59afaf7dca 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; -import static junit.framework.TestCase.assertTrue; import android.content.Context; import android.os.HandlerThread; @@ -45,7 +44,6 @@ import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -74,8 +72,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private Clock clock; private Timeline timeline; - private List mediaSources; private Object manifest; + private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private LoadControl loadControl; private BandwidthMeter bandwidthMeter; @@ -87,22 +85,18 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private AnalyticsListener analyticsListener; private Integer expectedPlayerEndedCount; - public Builder() { - mediaSources = new ArrayList<>(); - } - /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link * FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is - * not allowed after a call to {@link #setMediaSources(MediaSource...)}. + * not allowed after a call to {@link #setMediaSource(MediaSource)}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. * @return This builder. */ public Builder setTimeline(Timeline timeline) { - assertThat(mediaSources).isEmpty(); + assertThat(mediaSource).isNull(); this.timeline = timeline; return this; } @@ -110,30 +104,30 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value * is null. Setting the manifest is not allowed after a call to {@link - * #setMediaSources(MediaSource...)}. + * #setMediaSource(MediaSource)}. * * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. * @return This builder. */ public Builder setManifest(Object manifest) { - assertThat(mediaSources).isEmpty(); + assertThat(mediaSource).isNull(); this.manifest = manifest; return this; } /** - * Sets the {@link MediaSource}s to be used by the test runner. The default value is a {@link + * Sets a {@link MediaSource} to be used by the test runner. The default value is a {@link * FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)} - * and {@link #setManifest(Object)}. Setting media sources is not allowed after calls to {@link - * #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. + * and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to + * {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}. * - * @param mediaSources The {@link MediaSource}s to be used by the test runner. + * @param mediaSource A {@link MediaSource} to be used by the test runner. * @return This builder. */ - public Builder setMediaSources(MediaSource... mediaSources) { + public Builder setMediaSource(MediaSource mediaSource) { assertThat(timeline).isNull(); assertThat(manifest).isNull(); - this.mediaSources = Arrays.asList(mediaSources); + this.mediaSource = mediaSource; return this; } @@ -177,7 +171,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media * periods and for setting up a {@link FakeRenderer}. The default value is a single {@link * #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source - * with {@link #setMediaSources(MediaSource...)} and renderers with {@link + * with {@link #setMediaSource(MediaSource)} and renderers with {@link * #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. * * @param supportedFormats A list of supported {@link Format}s. @@ -231,7 +225,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be - * executed immediately before {@link SimpleExoPlayer#prepare()}. + * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. * * @param actionSchedule An {@link ActionSchedule} to be used by the test runner. * @return This builder. @@ -312,11 +306,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (clock == null) { clock = new AutoAdvancingFakeClock(); } - if (mediaSources.isEmpty()) { + if (mediaSource == null) { if (timeline == null) { timeline = new FakeTimeline(/* windowCount= */ 1, manifest); } - mediaSources.add(new FakeMediaSource(timeline, supportedFormats)); + mediaSource = new FakeMediaSource(timeline, supportedFormats); } if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; @@ -324,7 +318,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc return new ExoPlayerTestRunner( context, clock, - mediaSources, + mediaSource, renderersFactory, trackSelector, loadControl, @@ -338,7 +332,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final Context context; private final Clock clock; - private final List mediaSources; + private final MediaSource mediaSource; private final RenderersFactory renderersFactory; private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; @@ -355,7 +349,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final ArrayList timelineChangeReasons; private final ArrayList periodIndices; private final ArrayList discontinuityReasons; - private final ArrayList playbackStates; private SimpleExoPlayer player; private Exception exception; @@ -365,7 +358,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private ExoPlayerTestRunner( Context context, Clock clock, - List mediaSources, + MediaSource mediaSource, RenderersFactory renderersFactory, DefaultTrackSelector trackSelector, LoadControl loadControl, @@ -376,7 +369,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc int expectedPlayerEndedCount) { this.context = context; this.clock = clock; - this.mediaSources = mediaSources; + this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; this.loadControl = loadControl; @@ -388,7 +381,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); - this.playbackStates = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); @@ -434,10 +426,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.setMediaItems(mediaSources, /* resetPosition= */ false); - if (doPrepare) { - player.prepare(); - } + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } @@ -489,16 +478,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc /** * Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, - * int)} are the same to the provided timelines. This assert differs from testing equality by not - * comparing period ids which may be different due to id mapping of child source period ids. + * int)} are equal to the provided timelines. * * @param timelines A list of expected {@link Timeline}s. */ - public void assertTimelinesSame(Timeline... timelines) { - assertThat(this.timelines).hasSize(timelines.length); - for (int i = 0; i < timelines.length; i++) { - assertTrue(TestUtil.areTimelinesSame(timelines[i], this.timelines.get(i))); - } + public void assertTimelinesEqual(Timeline... timelines) { + assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); } /** @@ -510,15 +495,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder(); } - /** - * Asserts that the playback states reported by {@link - * Player.EventListener#onPlayerStateChanged(boolean, int)} are equal to the provided playback - * states. - */ - public void assertPlaybackStatesEqual(Integer... states) { - assertThat(playbackStates).containsExactlyElementsIn(Arrays.asList(states)).inOrder(); - } - /** * Asserts that the last track group array reported by {@link * Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the @@ -594,12 +570,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Override public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - timelineChangeReasons.add(reason); timelines.add(timeline); - int currentIndex = player.getCurrentPeriodIndex(); - if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) { - // Ignore timeline changes that do not change the period index. - periodIndices.add(currentIndex); + timelineChangeReasons.add(reason); + if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { + periodIndices.add(player.getCurrentPeriodIndex()); } } @@ -610,7 +584,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - playbackStates.add(playbackState); playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { @@ -657,9 +630,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc renderersFactory, trackSelector, loadControl, + /* drmSessionManager= */ null, bandwidthMeter, new AnalyticsCollector(clock), - /* useLazyPreparation= */ false, clock, Looper.myLooper()); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index a826e73e16..18eaec2cd7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -25,10 +25,8 @@ import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.util.List; /** * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} @@ -98,11 +96,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void prepare() { - throw new UnsupportedOperationException(); - } - @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -113,77 +106,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItems(List mediaItems) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItems( - List mediaItems, int startWindowIndex, long startPositionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItem(MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItem(int index, MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItems(List mediaSources) { - throw new UnsupportedOperationException(); - } - - @Override - public void addMediaItems(int index, List mediaSources) { - throw new UnsupportedOperationException(); - } - - @Override - public void moveMediaItem(int currentIndex, int newIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public MediaSource removeMediaItem(int index) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeMediaItems(int fromIndex, int toIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void clearMediaItems() { - throw new UnsupportedOperationException(); - } - @Override public void setPlayWhenReady(boolean playWhenReady) { throw new UnsupportedOperationException(); @@ -204,11 +126,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void setShuffleOrder(ShuffleOrder shuffleOrder) { - throw new UnsupportedOperationException(); - } - @Override public void setShuffleModeEnabled(boolean shuffleModeEnabled) { throw new UnsupportedOperationException(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 52c12f78b2..facfa0d7e4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -26,7 +26,6 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.DefaultDatabaseProvider; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; @@ -399,61 +398,4 @@ public class TestUtil { } return new DefaultExtractorInput(dataSource, position, length); } - - /** - * Checks whether the timelines are the same (does not compare {@link Timeline.Window#uid} and - * {@link Timeline.Period#uid}). - * - * @param firstTimeline The first {@link Timeline}. - * @param secondTimeline The second {@link Timeline} to compare with. - * @return {@code true} if both timelines are the same. - */ - public static boolean areTimelinesSame(Timeline firstTimeline, Timeline secondTimeline) { - if (firstTimeline == secondTimeline) { - return true; - } - if (secondTimeline.getWindowCount() != firstTimeline.getWindowCount() - || secondTimeline.getPeriodCount() != firstTimeline.getPeriodCount()) { - return false; - } - Timeline.Window firstWindow = new Timeline.Window(); - Timeline.Period firstPeriod = new Timeline.Period(); - Timeline.Window secondWindow = new Timeline.Window(); - Timeline.Period secondPeriod = new Timeline.Period(); - for (int i = 0; i < firstTimeline.getWindowCount(); i++) { - if (!areWindowsSame( - firstTimeline.getWindow(i, firstWindow), secondTimeline.getWindow(i, secondWindow))) { - return false; - } - } - for (int i = 0; i < firstTimeline.getPeriodCount(); i++) { - if (!firstTimeline - .getPeriod(i, firstPeriod, /* setIds= */ false) - .equals(secondTimeline.getPeriod(i, secondPeriod, /* setIds= */ false))) { - return false; - } - } - return true; - } - - /** - * Checks whether the windows are the same. This comparison does not compare the uid. - * - * @param first The first {@link Timeline.Window}. - * @param second The second {@link Timeline.Window}. - * @return true if both windows are the same. - */ - private static boolean areWindowsSame(Timeline.Window first, Timeline.Window second) { - return Util.areEqual(first.tag, second.tag) - && Util.areEqual(first.manifest, second.manifest) - && first.presentationStartTimeMs == second.presentationStartTimeMs - && first.windowStartTimeMs == second.windowStartTimeMs - && first.isSeekable == second.isSeekable - && first.isDynamic == second.isDynamic - && first.defaultPositionUs == second.defaultPositionUs - && first.durationUs == second.durationUs - && first.firstPeriodIndex == second.firstPeriodIndex - && first.lastPeriodIndex == second.lastPeriodIndex - && first.positionInFirstPeriodUs == second.positionInFirstPeriodUs; - } } From c5b6c6229be5d22736ef58c7b6dde7a95c7be352 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 24 Sep 2019 11:02:51 +0100 Subject: [PATCH 463/807] Simplify the ffmpeg build instructions a little In-line EXOPLAYER_ROOT which only has one reference. And change FFMPEG_EXT_PATH to always include "/jni" PiperOrigin-RevId: 270866662 --- extensions/ffmpeg/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index dd9ce38d35..3348f7cffb 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -25,8 +25,7 @@ follows: ``` cd "" -EXOPLAYER_ROOT="$(pwd)" -FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" +FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" ``` * Download the [Android NDK][] and set its location in an environment variable. @@ -69,7 +68,7 @@ COMMON_OPTIONS="\ --enable-decoder=opus \ --enable-decoder=flac \ " && \ -cd "${FFMPEG_EXT_PATH}/jni" && \ +cd "${FFMPEG_EXT_PATH}" && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ cd ffmpeg && git checkout release/4.0 && \ ./configure \ @@ -112,7 +111,7 @@ make clean built in the previous step. For example: ``` -cd "${FFMPEG_EXT_PATH}"/jni && \ +cd "${FFMPEG_EXT_PATH}" && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` From 4df2262bcff27ce4634adca3be49236a2a86be51 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 24 Sep 2019 16:35:36 +0100 Subject: [PATCH 464/807] Use Player.isPlaying in appropriate places. This method should be used where we previously checked for active playback by state==READY and playWhenReady=true. Using the new method ensures we take audio focus into account for these usages. Also update some method naming to avoid confusion with the isPlaying method. Issue:#6203 PiperOrigin-RevId: 270910982 --- .../exoplayer2/ui/PlayerControlView.java | 25 +++++++++------- .../ui/PlayerNotificationManager.java | 30 ++++++++----------- 2 files changed, 28 insertions(+), 27 deletions(-) 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 fd949db6a2..0e004b0df8 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 @@ -750,14 +750,14 @@ public class PlayerControlView extends FrameLayout { return; } boolean requestPlayPauseFocus = false; - boolean playing = isPlaying(); + boolean shouldShowPauseButton = shouldShowPauseButton(); if (playButton != null) { - requestPlayPauseFocus |= playing && playButton.isFocused(); - playButton.setVisibility(playing ? GONE : VISIBLE); + requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused(); + playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE); } if (pauseButton != null) { - requestPlayPauseFocus |= !playing && pauseButton.isFocused(); - pauseButton.setVisibility(!playing ? GONE : VISIBLE); + requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused(); + pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE); } if (requestPlayPauseFocus) { requestPlayPauseFocus(); @@ -945,7 +945,7 @@ public class PlayerControlView extends FrameLayout { // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); - if (playbackState == Player.STATE_READY && player.getPlayWhenReady()) { + if (player.isPlaying()) { long mediaTimeDelayMs = timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS; @@ -967,10 +967,10 @@ public class PlayerControlView extends FrameLayout { } private void requestPlayPauseFocus() { - boolean playing = isPlaying(); - if (!playing && playButton != null) { + boolean shouldShowPauseButton = shouldShowPauseButton(); + if (!shouldShowPauseButton && playButton != null) { playButton.requestFocus(); - } else if (playing && pauseButton != null) { + } else if (shouldShowPauseButton && pauseButton != null) { pauseButton.requestFocus(); } } @@ -1151,7 +1151,7 @@ public class PlayerControlView extends FrameLayout { return true; } - private boolean isPlaying() { + private boolean shouldShowPauseButton() { return player != null && player.getPlaybackState() != Player.STATE_ENDED && player.getPlaybackState() != Player.STATE_IDLE @@ -1221,6 +1221,11 @@ public class PlayerControlView extends FrameLayout { updateProgress(); } + @Override + public void onIsPlayingChanged(boolean isPlaying) { + updateProgress(); + } + @Override public void onRepeatModeChanged(int repeatMode) { updateRepeatModeButton(); 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 e4fcb37af3..a5823712f9 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 @@ -382,8 +382,6 @@ public class PlayerNotificationManager { private int visibility; @Priority private int priority; private boolean useChronometer; - private boolean wasPlayWhenReady; - private int lastPlaybackState; /** * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, @@ -663,8 +661,6 @@ public class PlayerNotificationManager { } this.player = player; if (player != null) { - wasPlayWhenReady = player.getPlayWhenReady(); - lastPlaybackState = player.getPlaybackState(); player.addListener(playerListener); startOrUpdateNotification(); } @@ -1070,10 +1066,9 @@ public class PlayerNotificationManager { // Changing "showWhen" causes notification flicker if SDK_INT < 21. if (Util.SDK_INT >= 21 && useChronometer + && player.isPlaying() && !player.isPlayingAd() - && !player.isCurrentWindowDynamic() - && player.getPlayWhenReady() - && player.getPlaybackState() == Player.STATE_READY) { + && !player.isCurrentWindowDynamic()) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) @@ -1138,7 +1133,7 @@ public class PlayerNotificationManager { stringActions.add(ACTION_REWIND); } if (usePlayPauseActions) { - if (isPlaying(player)) { + if (shouldShowPauseButton(player)) { stringActions.add(ACTION_PAUSE); } else { stringActions.add(ACTION_PLAY); @@ -1182,10 +1177,10 @@ public class PlayerNotificationManager { if (skipPreviousActionIndex != -1) { actionIndices[actionCounter++] = skipPreviousActionIndex; } - boolean isPlaying = isPlaying(player); - if (pauseActionIndex != -1 && isPlaying) { + boolean shouldShowPauseButton = shouldShowPauseButton(player); + if (pauseActionIndex != -1 && shouldShowPauseButton) { actionIndices[actionCounter++] = pauseActionIndex; - } else if (playActionIndex != -1 && !isPlaying) { + } else if (playActionIndex != -1 && !shouldShowPauseButton) { actionIndices[actionCounter++] = playActionIndex; } if (skipNextActionIndex != -1) { @@ -1257,7 +1252,7 @@ public class PlayerNotificationManager { controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); } - private boolean isPlaying(Player player) { + private boolean shouldShowPauseButton(Player player) { return player.getPlaybackState() != Player.STATE_ENDED && player.getPlaybackState() != Player.STATE_IDLE && player.getPlayWhenReady(); @@ -1328,11 +1323,12 @@ public class PlayerNotificationManager { @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) { - startOrUpdateNotification(); - wasPlayWhenReady = playWhenReady; - lastPlaybackState = playbackState; - } + startOrUpdateNotification(); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + startOrUpdateNotification(); } @Override From fa803967f238ced4cbd0b1303794065cf1e92284 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 24 Sep 2019 20:35:49 +0100 Subject: [PATCH 465/807] Remove mediaDrm methods from DefaultDrmSessionManager DrmSessionManagers may now be in released state, in which case they may release their MediaDrm. In that case, it would be invalid to forward method calls to the underlying MediaDrms. Users should instead call these methods directly on the MediaDrm. Issue:#4721 PiperOrigin-RevId: 270963393 --- .../drm/DefaultDrmSessionManager.java | 48 ------------------- .../exoplayer2/drm/OfflineLicenseHelper.java | 28 ----------- .../playbacktests/gts/DashTestRunner.java | 6 +-- 3 files changed, 3 insertions(+), 79 deletions(-) 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 6c90a3660d..d7324249e0 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 @@ -233,54 +233,6 @@ public class DefaultDrmSessionManager eventDispatcher.removeListener(eventListener); } - /** - * Provides access to {@link ExoMediaDrm#getPropertyString(String)}. - * - *

        This method may be called when the manager is in any state. - * - * @param key The key to request. - * @return The retrieved property. - */ - public final String getPropertyString(String key) { - return mediaDrm.getPropertyString(key); - } - - /** - * Provides access to {@link ExoMediaDrm#setPropertyString(String, String)}. - * - *

        This method may be called when the manager is in any state. - * - * @param key The property to write. - * @param value The value to write. - */ - public final void setPropertyString(String key, String value) { - mediaDrm.setPropertyString(key, value); - } - - /** - * Provides access to {@link ExoMediaDrm#getPropertyByteArray(String)}. - * - *

        This method may be called when the manager is in any state. - * - * @param key The key to request. - * @return The retrieved property. - */ - public final byte[] getPropertyByteArray(String key) { - return mediaDrm.getPropertyByteArray(key); - } - - /** - * Provides access to {@link ExoMediaDrm#setPropertyByteArray(String, byte[])}. - * - *

        This method may be called when the manager is in any state. - * - * @param key The property to write. - * @param value The value to write. - */ - public final void setPropertyByteArray(String key, byte[] value) { - mediaDrm.setPropertyByteArray(key, value); - } - /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be * called before {@link #acquireSession(Looper, DrmInitData)} or {@link 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 03c199bda0..d30b782b30 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 @@ -152,34 +152,6 @@ public final class OfflineLicenseHelper { drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener); } - /** - * @see DefaultDrmSessionManager#getPropertyByteArray - */ - public synchronized byte[] getPropertyByteArray(String key) { - return drmSessionManager.getPropertyByteArray(key); - } - - /** - * @see DefaultDrmSessionManager#setPropertyByteArray - */ - public synchronized void setPropertyByteArray(String key, byte[] value) { - drmSessionManager.setPropertyByteArray(key, value); - } - - /** - * @see DefaultDrmSessionManager#getPropertyString - */ - public synchronized String getPropertyString(String key) { - return drmSessionManager.getPropertyString(key); - } - - /** - * @see DefaultDrmSessionManager#setPropertyString - */ - public synchronized void setPropertyString(String key, String value) { - drmSessionManager.setPropertyString(key, value); - } - /** * Downloads an offline license. * 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 fb64a7d13b..052cd7d0a2 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 @@ -266,17 +266,17 @@ public final class DashTestRunner { try { MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory(userAgent)); + FrameworkMediaDrm frameworkMediaDrm = FrameworkMediaDrm.newInstance(WIDEVINE_UUID); DefaultDrmSessionManager drmSessionManager = new DefaultDrmSessionManager<>( C.WIDEVINE_UUID, - FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + frameworkMediaDrm, drmCallback, /* optionalKeyRequestParameters= */ null, /* multiSession= */ false, DefaultDrmSessionManager.INITIAL_DRM_REQUEST_RETRY_COUNT); if (!useL1Widevine) { - drmSessionManager.setPropertyString( - SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + frameworkMediaDrm.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); } if (offlineLicenseKeySetId != null) { drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, From e4cabcac0fa85f292e67557c28b4cc0bede5e9af Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 24 Sep 2019 23:32:01 +0100 Subject: [PATCH 466/807] Try initializing ADAPTATION_WORKAROUND_BUFFER as a byte[] PiperOrigin-RevId: 270999947 --- .../mediacodec/MediaCodecRenderer.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 14219b8dfd..211fc13ea5 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 @@ -303,13 +303,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; /** - * H.264/AVC buffer to queue when using the adaptation workaround (see - * {@link #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: - * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be - * queued to force a resolution change when adapting to a new format. + * H.264/AVC buffer to queue when using the adaptation workaround (see {@link + * #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: Baseline + * sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be queued to + * force a resolution change when adapting to a new format. */ - private static final byte[] ADAPTATION_WORKAROUND_BUFFER = Util.getBytesFromHexString( - "0000016742C00BDA259000000168CE0F13200000016588840DCE7118A0002FBF1C31C3275D78"); + private static final byte[] ADAPTATION_WORKAROUND_BUFFER = + new byte[] { + 0, 0, 1, 103, 66, -64, 11, -38, 37, -112, 0, 0, 1, 104, -50, 15, 19, 32, 0, 0, 1, 101, -120, + -124, 13, -50, 113, 24, -96, 0, 47, -65, 28, 49, -61, 39, 93, 120 + }; + private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; From c2bab7a7455467d6c2166871d04fe2f28f0143a8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 25 Sep 2019 16:18:30 +0100 Subject: [PATCH 467/807] Introduce ExoMediaDrm.Provider into DefaultDrmSessionManager Issue:#4721 PiperOrigin-RevId: 271127127 --- .../drm/DefaultDrmSessionManager.java | 79 ++++++++++++------- .../exoplayer2/drm/OfflineLicenseHelper.java | 4 + 2 files changed, 55 insertions(+), 28 deletions(-) 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 d7324249e0..7104e4bad7 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 @@ -87,7 +87,7 @@ public class DefaultDrmSessionManager private static final String TAG = "DefaultDrmSessionMgr"; private final UUID uuid; - private final ExoMediaDrm mediaDrm; + private final ExoMediaDrm.Provider exoMediaDrmProvider; private final MediaDrmCallback callback; @Nullable private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; @@ -98,6 +98,8 @@ public class DefaultDrmSessionManager private final List> sessions; private final List> provisioningSessions; + private int prepareCallsCount; + @Nullable private ExoMediaDrm exoMediaDrm; @Nullable private DefaultDrmSession placeholderDrmSession; @Nullable private Looper playbackLooper; private int mode; @@ -107,19 +109,19 @@ public class DefaultDrmSessionManager /** * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. */ public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters) { this( uuid, - mediaDrm, + exoMediaDrm, callback, optionalKeyRequestParameters, /* multiSession= */ false, @@ -128,7 +130,7 @@ public class DefaultDrmSessionManager /** * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. @@ -137,13 +139,13 @@ public class DefaultDrmSessionManager */ public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters, boolean multiSession) { this( uuid, - mediaDrm, + exoMediaDrm, callback, optionalKeyRequestParameters, multiSession, @@ -152,7 +154,7 @@ public class DefaultDrmSessionManager /** * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. @@ -163,14 +165,14 @@ public class DefaultDrmSessionManager */ public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount) { this( uuid, - mediaDrm, + new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm), callback, optionalKeyRequestParameters, multiSession, @@ -180,38 +182,26 @@ public class DefaultDrmSessionManager private DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm.Provider exoMediaDrmProvider, MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters, boolean multiSession, boolean allowPlaceholderSessions, LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkNotNull(uuid); - Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; - this.mediaDrm = mediaDrm; + this.exoMediaDrmProvider = exoMediaDrmProvider; this.callback = callback; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; - boolean canAcquirePlaceholderSessions = - !FrameworkMediaCrypto.class.equals(mediaDrm.getExoMediaCryptoType()) - || !FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; // TODO: Allow customization once this class has a Builder. - this.allowPlaceholderSessions = canAcquirePlaceholderSessions && allowPlaceholderSessions; + this.allowPlaceholderSessions = allowPlaceholderSessions; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; mode = MODE_PLAYBACK; sessions = new ArrayList<>(); provisioningSessions = new ArrayList<>(); - if (multiSession && C.WIDEVINE_UUID.equals(uuid) && Util.SDK_INT >= 19) { - // TODO: Enabling session sharing probably doesn't do anything useful here. It would only be - // useful if DefaultDrmSession instances were aware of one another's state, which is not - // implemented. Or if custom renderers are being used that allow playback to proceed before - // keys, which seems unlikely to be true in practice. - mediaDrm.setPropertyString("sessionSharing", "enable"); - } - mediaDrm.setOnEventListener(new MediaDrmEventListener()); } /** @@ -268,6 +258,30 @@ public class DefaultDrmSessionManager // DrmSessionManager implementation. + @Override + public final void prepare() { + if (prepareCallsCount++ == 0) { + Assertions.checkState(exoMediaDrm == null); + exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); + if (multiSession && C.WIDEVINE_UUID.equals(uuid) && Util.SDK_INT >= 19) { + // TODO: Enabling session sharing probably doesn't do anything useful here. It would only be + // useful if DefaultDrmSession instances were aware of one another's state, which is not + // implemented. Or if custom renderers are being used that allow playback to proceed before + // keys, which seems unlikely to be true in practice. + exoMediaDrm.setPropertyString("sessionSharing", "enable"); + } + exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); + } + } + + @Override + public final void release() { + if (--prepareCallsCount == 0) { + Assertions.checkNotNull(exoMediaDrm).release(); + exoMediaDrm = null; + } + } + @Override public boolean canAcquireSession(DrmInitData drmInitData) { if (offlineLicenseKeySetId != null) { @@ -304,7 +318,13 @@ public class DefaultDrmSessionManager @Nullable public DrmSession acquirePlaceholderSession(Looper playbackLooper) { assertExpectedPlaybackLooper(playbackLooper); - if (!allowPlaceholderSessions || mediaDrm.getExoMediaCryptoType() == null) { + Assertions.checkNotNull(exoMediaDrm); + boolean avoidPlaceholderDrmSessions = + FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) + && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; + if (avoidPlaceholderDrmSessions + || !allowPlaceholderSessions + || exoMediaDrm.getExoMediaCryptoType() == null) { return null; } maybeCreateMediaDrmHandler(playbackLooper); @@ -359,7 +379,9 @@ public class DefaultDrmSessionManager @Override @Nullable public Class getExoMediaCryptoType(DrmInitData drmInitData) { - return canAcquireSession(drmInitData) ? mediaDrm.getExoMediaCryptoType() : null; + return canAcquireSession(drmInitData) + ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType() + : null; } // ProvisioningManager implementation. @@ -408,9 +430,10 @@ public class DefaultDrmSessionManager private DefaultDrmSession createNewDefaultSession( @Nullable List schemeDatas, boolean isPlaceholderSession) { + Assertions.checkNotNull(exoMediaDrm); return new DefaultDrmSession<>( uuid, - mediaDrm, + exoMediaDrm, /* provisioningManager= */ this, /* releaseCallback= */ this::onSessionReleased, schemeDatas, 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 d30b782b30..79dc743bc9 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 @@ -201,6 +201,7 @@ public final class OfflineLicenseHelper { public synchronized Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); + drmSessionManager.prepare(); DrmSession drmSession = openBlockingKeyRequest( DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA); @@ -208,6 +209,7 @@ public final class OfflineLicenseHelper { Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); drmSession.releaseReference(); + drmSessionManager.release(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { return Pair.create(0L, 0L); @@ -227,11 +229,13 @@ public final class OfflineLicenseHelper { private byte[] blockingKeyRequest( @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { + drmSessionManager.prepare(); DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); drmSession.releaseReference(); + drmSessionManager.release(); if (error != null) { throw error; } From 60a9cf68c9249211f7c633a632c41c421d5f5f5b Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Wed, 25 Sep 2019 19:02:15 +0100 Subject: [PATCH 468/807] Add OpenGL support to av1 extension: jni library Update native gav1GetFrame method. PiperOrigin-RevId: 271160989 --- .../android/exoplayer2/video/VideoDecoderOutputBuffer.java | 2 ++ 1 file changed, 2 insertions(+) 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 index 3704a09da0..a3d3a7d967 100644 --- 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 @@ -23,10 +23,12 @@ import java.nio.ByteBuffer; /** Video decoder output buffer containing video frame data. */ public abstract class VideoDecoderOutputBuffer extends OutputBuffer { + // LINT.IfChange 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; + // LINT.ThenChange(../../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc) /** Decoder private data. */ public int decoderPrivate; From 004b9e8e8c660dba38ab458f60de7255c87c447b Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 26 Sep 2019 15:41:27 +0100 Subject: [PATCH 469/807] Add EventListener.onPlaybackSuppressionReasonChanged Adding this callback makes sense for completeness (we have similar callbacks for all other playback state properties), and also to detect audio focus loss while buffering which would currently trigger no callback because isPlaying is still false. Issue:#6203 PiperOrigin-RevId: 271347351 --- RELEASENOTES.md | 3 +++ .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 +++++- .../main/java/com/google/android/exoplayer2/Player.java | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 664a22e15e..89685bdce6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -66,6 +66,9 @@ ([#6161](https://github.com/google/ExoPlayer/issues/6161)). * Add demo app to show how to use the Android 10 `SurfaceControl` API with ExoPlayer ([#677](https://github.com/google/ExoPlayer/issues/677)). +* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to + detect playbacks suppressions (e.g. audio focus loss) directly + ([#6203](https://github.com/google/ExoPlayer/issues/6203)). ### 2.10.5 (2019-09-20) ### 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 cbbf5cacbc..dd8fbee53c 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 @@ -264,17 +264,21 @@ import java.util.concurrent.CopyOnWriteArrayList; internalPlayer.setPlayWhenReady(internalPlayWhenReady); } boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; + boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason; this.playWhenReady = playWhenReady; this.playbackSuppressionReason = playbackSuppressionReason; boolean isPlaying = isPlaying(); boolean isPlayingChanged = oldIsPlaying != isPlaying; - if (playWhenReadyChanged || isPlayingChanged) { + if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) { int playbackState = playbackInfo.playbackState; notifyListeners( listener -> { if (playWhenReadyChanged) { listener.onPlayerStateChanged(playWhenReady, playbackState); } + if (suppressionReasonChanged) { + listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); + } if (isPlayingChanged) { listener.onIsPlayingChanged(isPlaying); } 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 fafbd25c32..d809dcbc88 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 @@ -392,6 +392,14 @@ public interface Player { */ default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} + /** + * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes. + * + * @param playbackSuppressionReason The current {@link PlaybackSuppressionReason}. + */ + default void onPlaybackSuppressionReasonChanged( + @PlaybackSuppressionReason int playbackSuppressionReason) {} + /** * Called when the value of {@link #isPlaying()} changes. * From d185cea73b778d690f5a59f8e22b389402614eaa Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 26 Sep 2019 15:41:51 +0100 Subject: [PATCH 470/807] Forward isPlaying/playbackSuppressionReason changes to analytics listeners. PiperOrigin-RevId: 271347407 --- .../analytics/AnalyticsCollector.java | 18 ++++++++++++++++++ .../analytics/AnalyticsListener.java | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) 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 43154a4b3f..5a57844c83 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 @@ -22,6 +22,7 @@ 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.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; @@ -447,6 +448,23 @@ public class AnalyticsCollector } } + @Override + public void onPlaybackSuppressionReasonChanged( + @PlaybackSuppressionReason int playbackSuppressionReason) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason); + } + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onIsPlayingChanged(eventTime, isPlaying); + } + } + @Override public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); 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 656548df47..e16d92df9e 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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -132,6 +133,23 @@ public interface AnalyticsListener { default void onPlayerStateChanged( EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {} + /** + * Called when playback suppression reason changed. + * + * @param eventTime The event time. + * @param playbackSuppressionReason The new {@link PlaybackSuppressionReason}. + */ + default void onPlaybackSuppressionReasonChanged( + EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {} + + /** + * Called when the player starts or stops playing. + * + * @param eventTime The event time. + * @param isPlaying Whether the player is playing. + */ + default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {} + /** * Called when the timeline changed. * From 5a8c4b90f4f000b54ccace9bdf03387c1a0534ba Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 26 Sep 2019 15:49:31 +0100 Subject: [PATCH 471/807] Add getFlags implementation to DefaultDrmSessionManager Issue:#4867 PiperOrigin-RevId: 271348533 --- .../exoplayer2/drm/DefaultDrmSessionManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 7104e4bad7..10a11d95a8 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 @@ -93,6 +93,7 @@ public class DefaultDrmSessionManager private final EventDispatcher eventDispatcher; private final boolean multiSession; private final boolean allowPlaceholderSessions; + @Flags private final int flags; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final List> sessions; @@ -177,6 +178,7 @@ public class DefaultDrmSessionManager optionalKeyRequestParameters, multiSession, /* allowPlaceholderSessions= */ false, + /* flags= */ 0, new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } @@ -187,6 +189,7 @@ public class DefaultDrmSessionManager @Nullable HashMap optionalKeyRequestParameters, boolean multiSession, boolean allowPlaceholderSessions, + @Flags int flags, LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkNotNull(uuid); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); @@ -198,6 +201,7 @@ public class DefaultDrmSessionManager this.multiSession = multiSession; // TODO: Allow customization once this class has a Builder. this.allowPlaceholderSessions = allowPlaceholderSessions; + this.flags = flags; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; mode = MODE_PLAYBACK; sessions = new ArrayList<>(); @@ -376,6 +380,12 @@ public class DefaultDrmSessionManager return session; } + @Override + @Flags + public final int getFlags() { + return flags; + } + @Override @Nullable public Class getExoMediaCryptoType(DrmInitData drmInitData) { From b57c356fbf771cfffd7bd90b83f9836cad53909f Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 26 Sep 2019 17:16:29 +0100 Subject: [PATCH 472/807] Reshuffle {audio,video}SampleQueue{Index,MappingDone} into fields mapped by type PiperOrigin-RevId: 271364200 --- .../source/hls/HlsSampleStreamWrapper.java | 60 +++++++------------ 1 file changed, 23 insertions(+), 37 deletions(-) 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 e618d7f134..f87f411187 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.util.SparseIntArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -93,6 +94,10 @@ import java.util.Set; public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL = -2; public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL = -3; + private static final Set MAPPABLE_TYPES = + Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO))); + private final int trackType; private final Callback callback; private final HlsChunkSource chunkSource; @@ -114,10 +119,8 @@ import java.util.Set; private SampleQueue[] sampleQueues; private DecryptableSampleQueueReader[] sampleQueueReaders; private int[] sampleQueueTrackIds; - private boolean audioSampleQueueMappingDone; - private int audioSampleQueueIndex; - private boolean videoSampleQueueMappingDone; - private int videoSampleQueueIndex; + private Set sampleQueueMappingDoneByType; + private SparseIntArray sampleQueueIndicesByType; private int primarySampleQueueType; private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; @@ -188,8 +191,8 @@ import java.util.Set; loader = new Loader("Loader:HlsSampleStreamWrapper"); nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); sampleQueueTrackIds = new int[0]; - audioSampleQueueIndex = C.INDEX_UNSET; - videoSampleQueueIndex = C.INDEX_UNSET; + sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); + sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); sampleQueues = new SampleQueue[0]; sampleQueueReaders = new DecryptableSampleQueueReader[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; @@ -794,8 +797,7 @@ import java.util.Set; */ public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) { if (!reusingExtractor) { - audioSampleQueueMappingDone = false; - videoSampleQueueMappingDone = false; + sampleQueueMappingDoneByType.clear(); } this.chunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { @@ -814,30 +816,19 @@ import java.util.Set; public TrackOutput track(int id, int type) { int trackCount = sampleQueues.length; - // Audio and video tracks are handled manually to ignore ids. - if (type == C.TRACK_TYPE_AUDIO) { - if (audioSampleQueueIndex != C.INDEX_UNSET) { - if (audioSampleQueueMappingDone) { - return sampleQueueTrackIds[audioSampleQueueIndex] == id - ? sampleQueues[audioSampleQueueIndex] + if (MAPPABLE_TYPES.contains(type)) { + // Track types in MAPPABLE_TYPES are handled manually to ignore IDs. + int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET); + if (sampleQueueIndex != C.INDEX_UNSET) { + if (sampleQueueMappingDoneByType.contains(type)) { + return sampleQueueTrackIds[sampleQueueIndex] == id + ? sampleQueues[sampleQueueIndex] : createDummyTrackOutput(id, type); + } else { + sampleQueueMappingDoneByType.add(type); + sampleQueueTrackIds[sampleQueueIndex] = id; + return sampleQueues[sampleQueueIndex]; } - audioSampleQueueMappingDone = true; - sampleQueueTrackIds[audioSampleQueueIndex] = id; - return sampleQueues[audioSampleQueueIndex]; - } else if (tracksEnded) { - return createDummyTrackOutput(id, type); - } - } else if (type == C.TRACK_TYPE_VIDEO) { - if (videoSampleQueueIndex != C.INDEX_UNSET) { - if (videoSampleQueueMappingDone) { - return sampleQueueTrackIds[videoSampleQueueIndex] == id - ? sampleQueues[videoSampleQueueIndex] - : createDummyTrackOutput(id, type); - } - videoSampleQueueMappingDone = true; - sampleQueueTrackIds[videoSampleQueueIndex] = id; - return sampleQueues[videoSampleQueueIndex]; } else if (tracksEnded) { return createDummyTrackOutput(id, type); } @@ -866,13 +857,8 @@ import java.util.Set; sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; - if (type == C.TRACK_TYPE_AUDIO) { - audioSampleQueueMappingDone = true; - audioSampleQueueIndex = trackCount; - } else if (type == C.TRACK_TYPE_VIDEO) { - videoSampleQueueMappingDone = true; - videoSampleQueueIndex = trackCount; - } + sampleQueueMappingDoneByType.add(type); + sampleQueueIndicesByType.append(type, trackCount); if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) { primarySampleQueueIndex = trackCount; primarySampleQueueType = type; From f0b9889ef6e218b1a41bbf18cb0948135fd994ee Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 26 Sep 2019 17:18:03 +0100 Subject: [PATCH 473/807] Remove duplicated tracksEnded check in HlsSampleStreamWrapper PiperOrigin-RevId: 271364512 --- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 f87f411187..56e913b357 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 @@ -829,8 +829,6 @@ import java.util.Set; sampleQueueTrackIds[sampleQueueIndex] = id; return sampleQueues[sampleQueueIndex]; } - } else if (tracksEnded) { - return createDummyTrackOutput(id, type); } } else /* sparse track */ { for (int i = 0; i < trackCount; i++) { @@ -838,9 +836,9 @@ import java.util.Set; return sampleQueues[i]; } } - if (tracksEnded) { - return createDummyTrackOutput(id, type); - } + } + if (tracksEnded) { + return createDummyTrackOutput(id, type); } SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData); trackOutput.setSampleOffsetUs(sampleOffsetUs); From d632cb86c13edc398bcd0400a5bee0e3f6bad808 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 26 Sep 2019 20:21:51 +0100 Subject: [PATCH 474/807] Make multisession work well with placeholder session Issue:#4867 PiperOrigin-RevId: 271404942 --- .../exoplayer2/drm/DefaultDrmSessionManager.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 10a11d95a8..92cb5d7b8b 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 @@ -102,6 +102,7 @@ public class DefaultDrmSessionManager private int prepareCallsCount; @Nullable private ExoMediaDrm exoMediaDrm; @Nullable private DefaultDrmSession placeholderDrmSession; + @Nullable private DefaultDrmSession noMultiSessionDrmSession; @Nullable private Looper playbackLooper; private int mode; @Nullable private byte[] offlineLicenseKeySetId; @@ -357,9 +358,9 @@ public class DefaultDrmSessionManager } } - DefaultDrmSession session; + @Nullable DefaultDrmSession session; if (!multiSession) { - session = sessions.isEmpty() ? null : sessions.get(0); + session = noMultiSessionDrmSession; } else { // Only use an existing session if it has matching init data. session = null; @@ -374,6 +375,9 @@ public class DefaultDrmSessionManager if (session == null) { // Create a new session. session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false); + if (!multiSession) { + noMultiSessionDrmSession = session; + } sessions.add(session); } session.acquireReference(); @@ -462,6 +466,9 @@ public class DefaultDrmSessionManager if (placeholderDrmSession == drmSession) { placeholderDrmSession = null; } + if (noMultiSessionDrmSession == drmSession) { + noMultiSessionDrmSession = null; + } if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { // Other sessions were waiting for the released session to complete a provision operation. // We need to have one of those sessions perform the provision operation instead. From 22c3be75ea9c07b8c7e076041f6c1634d029ebba Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Fri, 27 Sep 2019 15:45:55 +0100 Subject: [PATCH 475/807] Extract vpx code used for GL rendering to common classes This will be used by both vp9 and av1 Exoplayer extensions. PiperOrigin-RevId: 271568429 --- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 3 +- .../ext/vp9/LibvpxVideoRenderer.java | 15 +++---- .../exoplayer2/ext/vp9/VpxDecoder.java | 24 ++++++----- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 26 +++++++----- extensions/vp9/src/main/jni/vpx_jni.cc | 40 +++++++++---------- library/core/proguard-rules.txt | 5 +++ .../video/VideoDecoderOutputBuffer.java | 29 +++++++++++++- .../VideoDecoderOutputBufferRenderer.java | 11 ++--- .../video/VideoDecoderRenderer.java | 31 +++++++------- .../video/VideoDecoderSurfaceView.java | 19 +++++---- 10 files changed, 121 insertions(+), 82 deletions(-) rename extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java => library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java (78%) rename extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java => library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java (87%) rename extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java => library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderSurfaceView.java (67%) 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 d4e0795293..ff5d60a74a 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.video.VideoDecoderSurfaceView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -122,7 +123,7 @@ public class VpxPlaybackTest { player .createMessage(videoRenderer) .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) - .setPayload(new VpxVideoSurfaceView(context)) + .setPayload(new VideoDecoderSurfaceView(context)) .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); 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 13f020031c..79d1d2e66f 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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; import com.google.android.exoplayer2.video.VideoDecoderException; import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -48,8 +49,8 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; *

      • Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload * should be the target {@link Surface}, or null. *
      • Message with type {@link #MSG_SET_OUTPUT_BUFFER_RENDERER} to set the output buffer - * renderer. The message payload should be the target {@link VpxOutputBufferRenderer}, or - * null. + * renderer. The message payload should be the target {@link + * VideoDecoderOutputBufferRenderer}, or null. * */ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { @@ -57,7 +58,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { /** * The type of a message that can be passed to an instance of this class via {@link * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link - * VpxOutputBufferRenderer}, or null. + * VideoDecoderOutputBufferRenderer}, or null. */ public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE; @@ -79,11 +80,11 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { private final int threads; private Surface surface; - private VpxOutputBufferRenderer outputBufferRenderer; + private VideoDecoderOutputBufferRenderer outputBufferRenderer; @C.VideoOutputMode private int outputMode; private VpxDecoder decoder; - private VpxOutputBuffer outputBuffer; + private VideoDecoderOutputBuffer outputBuffer; private VideoFrameMetadataListener frameMetadataListener; @@ -298,7 +299,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { if (messageType == C.MSG_SET_SURFACE) { setOutput((Surface) message, null); } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) { - setOutput(null, (VpxOutputBufferRenderer) message); + setOutput(null, (VideoDecoderOutputBufferRenderer) message); } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { frameMetadataListener = (VideoFrameMetadataListener) message; } else { @@ -309,7 +310,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { // Internal methods. private void setOutput( - @Nullable Surface surface, @Nullable VpxOutputBufferRenderer outputBufferRenderer) { + @Nullable Surface surface, @Nullable VideoDecoderOutputBufferRenderer outputBufferRenderer) { // At most one output may be non-null. Both may be null if the output is being cleared. Assertions.checkState(surface == null || outputBufferRenderer == null); if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) { 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 f6b1ddccea..d6ab6efc8d 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 @@ -24,11 +24,12 @@ import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; import java.nio.ByteBuffer; /** Vpx decoder. */ /* package */ final class VpxDecoder - extends SimpleDecoder { + extends SimpleDecoder { // These constants should match the codes returned from vpxDecode and vpxSecureDecode functions in // https://github.com/google/ExoPlayer/blob/release-v2/extensions/vp9/src/main/jni/vpx_jni.cc. @@ -63,7 +64,9 @@ import java.nio.ByteBuffer; boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { - super(new VideoDecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); + super( + new VideoDecoderInputBuffer[numInputBuffers], + new VideoDecoderOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); } @@ -98,12 +101,12 @@ import java.nio.ByteBuffer; } @Override - protected VpxOutputBuffer createOutputBuffer() { - return new VpxOutputBuffer(this); + protected VideoDecoderOutputBuffer createOutputBuffer() { + return new VideoDecoderOutputBuffer(this::releaseOutputBuffer); } @Override - protected void releaseOutputBuffer(VpxOutputBuffer buffer) { + protected void releaseOutputBuffer(VideoDecoderOutputBuffer 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 == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) { @@ -120,7 +123,7 @@ import java.nio.ByteBuffer; @Override @Nullable protected VpxDecoderException decode( - VideoDecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { + VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = Util.castNonNull(inputBuffer.data); int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; @@ -163,7 +166,7 @@ import java.nio.ByteBuffer; } /** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */ - public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface) + public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VpxDecoderException { int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer); if (getFrameResult == -1) { @@ -189,19 +192,20 @@ import java.nio.ByteBuffer; int[] numBytesOfClearData, int[] numBytesOfEncryptedData); - private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); + private native int vpxGetFrame(long context, VideoDecoderOutputBuffer outputBuffer); /** * Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called * if {@link #vpxInit} was called with {@code enableBufferManager = true}. */ - private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer); + private native int vpxRenderFrame( + long context, Surface surface, VideoDecoderOutputBuffer outputBuffer); /** * Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link * #vpxInit} was called with {@code enableBufferManager = true}. */ - private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer); + private native int vpxReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer); private native int vpxGetErrorCode(long context); private native String vpxGetErrorMessage(long context); 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 7177cde12e..1c434032d0 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 @@ -17,18 +17,22 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; -/** Video output buffer, populated by {@link VpxDecoder}. */ +// TODO(b/139174707): Delete this class once binaries in WVVp9OpusPlaybackTest are updated to depend +// on VideoDecoderOutputBuffer. Also mark VideoDecoderOutputBuffer as final. +/** + * Video output buffer, populated by {@link VpxDecoder}. + * + * @deprecated Use {@link VideoDecoderOutputBuffer} instead. + */ +@Deprecated public final class VpxOutputBuffer extends VideoDecoderOutputBuffer { - private final VpxDecoder owner; - - public VpxOutputBuffer(VpxDecoder owner) { - this.owner = owner; + /** + * Creates VpxOutputBuffer. + * + * @param owner Buffer owner. + */ + public VpxOutputBuffer(VideoDecoderOutputBuffer.Owner owner) { + super(owner); } - - @Override - public void release() { - owner.releaseOutputBuffer(this); - } - } diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 303672334d..9c4b6d4acf 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -38,27 +38,27 @@ #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \ __VA_ARGS__)) -#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_vp9_VpxDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ - } \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_vp9_VpxDecoder_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ +#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_vp9_VpxDecoder_##NAME( \ + JNIEnv* env, jobject thiz, ##__VA_ARGS__); \ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_vp9_VpxDecoder_##NAME( \ + JNIEnv* env, jobject thiz, ##__VA_ARGS__) -#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_vp9_VpxLibrary_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ - } \ - JNIEXPORT RETURN_TYPE \ - Java_com_google_android_exoplayer2_ext_vp9_VpxLibrary_ ## NAME \ - (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ +#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_vp9_VpxLibrary_##NAME( \ + JNIEnv* env, jobject thiz, ##__VA_ARGS__); \ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer2_ext_vp9_VpxLibrary_##NAME( \ + JNIEnv* env, jobject thiz, ##__VA_ARGS__) -// JNI references for VpxOutputBuffer class. +// JNI references for VideoDecoderOutputBuffer class. static jmethodID initForYuvFrame; static jmethodID initForPrivateFrame; static jfieldID dataField; @@ -477,7 +477,7 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, // Populate JNI References. const jclass outputBufferClass = env->FindClass( - "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); + "com/google/android/exoplayer2/video/VideoDecoderOutputBuffer"); initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z"); initForPrivateFrame = diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index ab3cc5fccd..67646be956 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -61,3 +61,8 @@ # Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** -dontwarn kotlin.annotations.jvm.** + +# Some members of this class are being accessed from native methods. Keep them unobfuscated. +-keep class com.google.android.exoplayer2.ext.video.VideoDecoderOutputBuffer { + *; +} 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 index a3d3a7d967..299b5656b8 100644 --- 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 @@ -21,7 +21,18 @@ 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 class VideoDecoderOutputBuffer extends OutputBuffer { + + /** Buffer owner. */ + public interface Owner { + + /** + * Releases the buffer. + * + * @param outputBuffer Output buffer. + */ + public void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer); + } // LINT.IfChange public static final int COLORSPACE_UNKNOWN = 0; @@ -54,6 +65,22 @@ public abstract class VideoDecoderOutputBuffer extends OutputBuffer { */ @Nullable public ByteBuffer supplementalData; + private final Owner owner; + + /** + * Creates VideoDecoderOutputBuffer. + * + * @param owner Buffer owner. + */ + public VideoDecoderOutputBuffer(Owner owner) { + this.owner = owner; + } + + @Override + public void release() { + owner.releaseOutputBuffer(this); + } + /** * Initializes the buffer. * diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java similarity index 78% rename from extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java index d07e24d920..c57794f454 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java @@ -13,18 +13,15 @@ * 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; -/** - * Renders the {@link VpxOutputBuffer}. - */ -public interface VpxOutputBufferRenderer { +/** Renders the {@link VideoDecoderOutputBuffer}. */ +public interface VideoDecoderOutputBufferRenderer { /** * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. * * @param outputBuffer The output buffer to be rendered. */ - void setOutputBuffer(VpxOutputBuffer outputBuffer); - + void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer); } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java similarity index 87% rename from extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java index d82f5a6071..ab338a0af5 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java @@ -13,7 +13,7 @@ * 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 android.opengl.GLES20; import android.opengl.GLSurfaceView; @@ -24,10 +24,10 @@ import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * GLSurfaceView.Renderer implementation that can render YUV Frames returned by libvpx after - * decoding. It does the YUV to RGB color conversion in the Fragment Shader. + * GLSurfaceView.Renderer implementation that can render YUV Frames returned by a video decoder + * after decoding. It does the YUV to RGB color conversion in the Fragment Shader. */ -/* package */ class VpxRenderer implements GLSurfaceView.Renderer { +/* package */ class VideoDecoderRenderer implements GLSurfaceView.Renderer { private static final float[] kColorConversion601 = { 1.164f, 1.164f, 1.164f, @@ -74,7 +74,7 @@ import javax.microedition.khronos.opengles.GL10; private static final FloatBuffer TEXTURE_VERTICES = GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f}); private final int[] yuvTextures = new int[3]; - private final AtomicReference pendingOutputBufferReference; + private final AtomicReference pendingOutputBufferReference; // Kept in a field rather than a local variable so that it doesn't get garbage collected before // glDrawArrays uses it. @@ -86,9 +86,9 @@ import javax.microedition.khronos.opengles.GL10; private int previousWidth; private int previousStride; - private VpxOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. + private VideoDecoderOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. - public VpxRenderer() { + public VideoDecoderRenderer() { previousWidth = -1; previousStride = -1; pendingOutputBufferReference = new AtomicReference<>(); @@ -96,12 +96,13 @@ import javax.microedition.khronos.opengles.GL10; /** * Set a frame to be rendered. This should be followed by a call to - * VpxVideoSurfaceView.requestRender() to actually render the frame. + * VideoDecoderSurfaceView.requestRender() to actually render the frame. * * @param outputBuffer OutputBuffer containing the YUV Frame to be rendered */ - public void setFrame(VpxOutputBuffer outputBuffer) { - VpxOutputBuffer oldPendingOutputBuffer = pendingOutputBufferReference.getAndSet(outputBuffer); + public void setFrame(VideoDecoderOutputBuffer outputBuffer) { + VideoDecoderOutputBuffer oldPendingOutputBuffer = + pendingOutputBufferReference.getAndSet(outputBuffer); if (oldPendingOutputBuffer != null) { // The old pending output buffer will never be used for rendering, so release it now. oldPendingOutputBuffer.release(); @@ -132,7 +133,7 @@ import javax.microedition.khronos.opengles.GL10; @Override public void onDrawFrame(GL10 unused) { - VpxOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); + VideoDecoderOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); if (pendingOutputBuffer == null && renderedOutputBuffer == null) { // There is no output buffer to render at the moment. return; @@ -143,17 +144,17 @@ import javax.microedition.khronos.opengles.GL10; } renderedOutputBuffer = pendingOutputBuffer; } - VpxOutputBuffer outputBuffer = renderedOutputBuffer; + VideoDecoderOutputBuffer outputBuffer = renderedOutputBuffer; // Set color matrix. Assume BT709 if the color space is unknown. float[] colorConversion = kColorConversion709; switch (outputBuffer.colorspace) { - case VpxOutputBuffer.COLORSPACE_BT601: + case VideoDecoderOutputBuffer.COLORSPACE_BT601: colorConversion = kColorConversion601; break; - case VpxOutputBuffer.COLORSPACE_BT2020: + case VideoDecoderOutputBuffer.COLORSPACE_BT2020: colorConversion = kColorConversion2020; break; - case VpxOutputBuffer.COLORSPACE_BT709: + case VideoDecoderOutputBuffer.COLORSPACE_BT709: default: break; // Do nothing } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderSurfaceView.java similarity index 67% rename from extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderSurfaceView.java index 9dd2432622..f2a4c2d002 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderSurfaceView.java @@ -13,27 +13,26 @@ * 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 android.content.Context; import android.opengl.GLSurfaceView; import android.util.AttributeSet; import androidx.annotation.Nullable; -/** - * A GLSurfaceView extension that scales itself to the given aspect ratio. - */ -public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBufferRenderer { +/** A GLSurfaceView extension that scales itself to the given aspect ratio. */ +public class VideoDecoderSurfaceView extends GLSurfaceView + implements VideoDecoderOutputBufferRenderer { - private final VpxRenderer renderer; + private final VideoDecoderRenderer renderer; - public VpxVideoSurfaceView(Context context) { + public VideoDecoderSurfaceView(Context context) { this(context, /* attrs= */ null); } - public VpxVideoSurfaceView(Context context, @Nullable AttributeSet attrs) { + public VideoDecoderSurfaceView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - renderer = new VpxRenderer(); + renderer = new VideoDecoderRenderer(); setPreserveEGLContextOnPause(true); setEGLContextClientVersion(2); setRenderer(renderer); @@ -41,7 +40,7 @@ public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBuffe } @Override - public void setOutputBuffer(VpxOutputBuffer outputBuffer) { + public void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { renderer.setFrame(outputBuffer); requestRender(); } From b50da6d72e0293f44c21545bfc0f942b5e70b151 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 27 Sep 2019 16:42:58 +0100 Subject: [PATCH 476/807] Add DefaultDrmSessionManager.Builder Issue:#6334 Issue:#4721 Issue:#6334 Issue:#4867 PiperOrigin-RevId: 271577773 --- RELEASENOTES.md | 16 +- .../drm/DefaultDrmSessionManager.java | 144 +++++++++++++++++- 2 files changed, 154 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 89685bdce6..0fef76f737 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,13 +2,22 @@ ### dev-v2 (not yet released) ### +* DRM: + * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` + ([#5619](https://github.com/google/ExoPlayer/issues/5619)). + * Add a `DefaultDrmSessionManager.Builder`. + * Add support for the use of secure decoders in clear sections of content + ([#4867](https://github.com/google/ExoPlayer/issues/4867)). + * Add basic DRM support to the Cast demo app. + * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning + requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). + * Remove `DefaultDrmSessionManager` factory methods that leak `ExoMediaDrm` + instances ([#4721](https://github.com/google/ExoPlayer/issues/4721)). * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Instead, set the header `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` in the `DataSpec` `httpRequestHeaders`. * DASH: Support negative @r values in segment timelines ([#1787](https://github.com/google/ExoPlayer/issues/1787)). -* Remove `DefaultDrmSessionManager` factory methods that leak `ExoMediaDrm` - instances ([#4721](https://github.com/google/ExoPlayer/issues/4721)). * Add `allowedCapturePolicy` field to `AudioAttributes` wrapper to allow to opt-out of audio recording. * Add `DataSpec.httpRequestHeaders` to set HTTP request headers when connecting @@ -24,7 +33,6 @@ display by default. * Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis and analytics reporting (TODO: link to developer guide page/blog post). -* Add basic DRM support to the Cast demo app. * Assume that encrypted content requires secure decoders in renderer support checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Decoders: Prefer decoders that advertise format support over ones that do not, @@ -50,8 +58,6 @@ the `Player` set later using `AnalyticsCollector.setPlayer`. * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and `ExoPlayer.Builder`. -* Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` - ([#5619](https://github.com/google/ExoPlayer/issues/5619)). * Fix issue where player errors are thrown too early at playlist transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be set. 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 92cb5d7b8b..e8a6fe6572 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 @@ -39,6 +39,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; /** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @@ -46,6 +47,148 @@ import java.util.UUID; public class DefaultDrmSessionManager implements DrmSessionManager, ProvisioningManager { + /** + * Builder for {@link DefaultDrmSessionManager} instances. + * + *

        See {@link #Builder} for the list of default values. + */ + public static final class Builder { + + private final HashMap keyRequestParameters; + private UUID uuid; + private ExoMediaDrm.Provider exoMediaDrmProvider; + private boolean multiSession; + private boolean allowPlaceholderSessions; + @Flags private int flags; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + + /** + * Creates a builder with default values. + * + *

          + *
        • {@link #setKeyRequestParameters keyRequestParameters}: An empty map. + *
        • {@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}. + *
        • {@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link + * FrameworkMediaDrm#DEFAULT_PROVIDER}. + *
        • {@link #setMultiSession multiSession}: Not allowed by default. + *
        • {@link #setAllowPlaceholderSessions allowPlaceholderSession}: Not allowed by default. + *
        • {@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: Not allowed by + * default. + *
        • {@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link + * DefaultLoadErrorHandlingPolicy}. + *
        + */ + @SuppressWarnings("unchecked") + public Builder() { + keyRequestParameters = new HashMap<>(); + uuid = C.WIDEVINE_UUID; + exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; + multiSession = false; + allowPlaceholderSessions = false; + flags = 0; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + } + + /** + * Sets the parameters to pass to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. + * + * @param keyRequestParameters A map with parameters. + * @return This builder. + */ + public Builder setKeyRequestParameters(Map keyRequestParameters) { + this.keyRequestParameters.clear(); + this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters)); + return this; + } + + /** + * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use. + * + * @param uuid The UUID of the DRM scheme. + * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}. + * @return This builder. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public Builder setUuidAndExoMediaDrmProvider( + UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { + this.uuid = Assertions.checkNotNull(uuid); + this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider); + return this; + } + + /** + * Sets whether this session manager is allowed to acquire multiple simultaneous sessions. + * + *

        Users should pass false when a single key request will obtain all keys required to decrypt + * the associated content. {@code multiSession} is required when content uses key rotation. + * + * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous + * sessions. + * @return This builder. + */ + public Builder setMultiSession(boolean multiSession) { + this.multiSession = multiSession; + return this; + } + + /** + * Sets whether this session manager is allowed to acquire placeholder sessions. + * + *

        Placeholder sessions allow the use of secure renderers to play clear content. + * + * @param allowPlaceholderSessions Whether this session manager is allowed to acquire + * placeholder sessions. + * @return This builder. + */ + public Builder setAllowPlaceholderSessions(boolean allowPlaceholderSessions) { + this.allowPlaceholderSessions = allowPlaceholderSessions; + return this; + } + + /** + * Sets whether clear samples should be played when keys are not available. Keys are considered + * unavailable when the load request is taking place, or when the key request has failed. + * + *

        This option does not affect placeholder sessions. + * + * @param playClearSamplesWithoutKeys Whether clear samples should be played when keys are not + * available. + * @return This builder. + */ + public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) { + if (playClearSamplesWithoutKeys) { + this.flags |= FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS; + } else { + this.flags &= ~FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS; + } + return this; + } + + /** + * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This builder. + */ + public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy); + return this; + } + + /** Builds a {@link DefaultDrmSessionManager} instance. */ + public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) { + return new DefaultDrmSessionManager<>( + uuid, + exoMediaDrmProvider, + mediaDrmCallback, + keyRequestParameters, + multiSession, + allowPlaceholderSessions, + flags, + loadErrorHandlingPolicy); + } + } + /** * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does * not contain scheme data for the required UUID. @@ -200,7 +343,6 @@ public class DefaultDrmSessionManager this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; - // TODO: Allow customization once this class has a Builder. this.allowPlaceholderSessions = allowPlaceholderSessions; this.flags = flags; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; From 9f8002f16a7cfa49607cff1c59e06df7d9016bb8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 27 Sep 2019 19:56:29 +0100 Subject: [PATCH 477/807] Update LINT.IfChange for extension constants PiperOrigin-RevId: 271617996 --- extensions/vp9/src/main/jni/vpx_jni.cc | 4 ++++ .../android/exoplayer2/video/VideoDecoderOutputBuffer.java | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 9c4b6d4acf..823f9b8cab 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -532,15 +532,19 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { return 1; } + // LINT.IfChange const int kOutputModeYuv = 0; const int kOutputModeSurfaceYuv = 1; + // LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/C.java) int outputMode = env->GetIntField(jOutputBuffer, outputModeField); if (outputMode == kOutputModeYuv) { + // LINT.IfChange const int kColorspaceUnknown = 0; const int kColorspaceBT601 = 1; const int kColorspaceBT709 = 2; const int kColorspaceBT2020 = 3; + // LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java) int colorspace = kColorspaceUnknown; switch (img->cs) { 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 index 299b5656b8..44ab168505 100644 --- 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 @@ -31,15 +31,13 @@ public class VideoDecoderOutputBuffer extends OutputBuffer { * * @param outputBuffer Output buffer. */ - public void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer); + void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer); } - // LINT.IfChange 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; - // LINT.ThenChange(../../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc) /** Decoder private data. */ public int decoderPrivate; From 36d3fef2b5ee803fc7a5e41befc27bd8fd6a8b4e Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Mon, 30 Sep 2019 10:58:32 +0100 Subject: [PATCH 478/807] Update vpx README file PiperOrigin-RevId: 271942692 --- extensions/vp9/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 34230db2ec..b7ea254a6c 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -107,12 +107,11 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibvpxVideoRenderer` to the player, then implement your own logic to use the renderer for a given track. -`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not -being used via `SimpleExoPlayer`, in which case color space conversion will be -performed using a GL shader. To enable this mode, send the renderer a message of -type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the -`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a -`Surface`. +`LibvpxVideoRenderer` can optionally output to a `VideoDecoderSurfaceView` when +not being used via `SimpleExoPlayer`, in which case color space conversion will +be performed using a GL shader. To enable this mode, send the renderer a message +of type `C.MSG_SET_OUTPUT_BUFFER_RENDERER` with the `VideoDecoderSurfaceView` as +its object, instead of sending `MSG_SET_SURFACE` with a `Surface`. ## Links ## From bab12af5973e67ee0345a1257e0f714f70b6e223 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 30 Sep 2019 11:46:44 +0100 Subject: [PATCH 479/807] Tweak Id3Decoder error message to print hex instead of int Before: "Unexpected first three bytes of ID3 tag header: 6845556" After: "Unexpected first three bytes of ID3 tag header: 0x687474" PiperOrigin-RevId: 271949486 --- .../com/google/android/exoplayer2/metadata/id3/Id3Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c8755f9aee..ba0968cbd4 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 @@ -163,7 +163,7 @@ public final class Id3Decoder implements MetadataDecoder { int id = data.readUnsignedInt24(); if (id != ID3_TAG) { - Log.w(TAG, "Unexpected first three bytes of ID3 tag header: " + id); + Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id)); return null; } From 7c199eb1ad86302e4ecd0adec3257ba44b5ef3bb Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Mon, 30 Sep 2019 11:49:40 +0100 Subject: [PATCH 480/807] Move vpx constant for setting output renderer to common constants file This will be used by both av1 and vp9 extensions. PiperOrigin-RevId: 271949754 --- RELEASENOTES.md | 5 +++++ .../android/exoplayer2/ext/vp9/VpxPlaybackTest.java | 3 ++- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 11 ++--------- .../main/java/com/google/android/exoplayer2/C.java | 9 +++++++++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0fef76f737..349b904589 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -75,6 +75,11 @@ * Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to detect playbacks suppressions (e.g. audio focus loss) directly ([#6203](https://github.com/google/ExoPlayer/issues/6203)). +* VP9 extension: + * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` + and move it to the core library. + * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to + `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. ### 2.10.5 (2019-09-20) ### 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 ff5d60a74a..3dd039118c 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 @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.Looper; 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; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; @@ -122,7 +123,7 @@ public class VpxPlaybackTest { .createMediaSource(uri); player .createMessage(videoRenderer) - .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) + .setType(C.MSG_SET_OUTPUT_BUFFER_RENDERER) .setPayload(new VideoDecoderSurfaceView(context)) .send(); player.prepare(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 79d1d2e66f..fe59031429 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 @@ -48,20 +48,13 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; *

          *
        • Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload * should be the target {@link Surface}, or null. - *
        • Message with type {@link #MSG_SET_OUTPUT_BUFFER_RENDERER} to set the output buffer + *
        • Message with type {@link C#MSG_SET_OUTPUT_BUFFER_RENDERER} to set the output buffer * renderer. The message payload should be the target {@link * VideoDecoderOutputBufferRenderer}, or null. *
        */ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { - /** - * The type of a message that can be passed to an instance of this class via {@link - * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link - * VideoDecoderOutputBufferRenderer}, or null. - */ - public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE; - /** The number of input buffers. */ private final int numInputBuffers; /** @@ -298,7 +291,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { setOutput((Surface) message, null); - } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) { + } else if (messageType == C.MSG_SET_OUTPUT_BUFFER_RENDERER) { setOutput(null, (VideoDecoderOutputBufferRenderer) message); } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { frameMetadataListener = (VideoFrameMetadataListener) message; 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 c8a73fbe9a..b235715f46 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 @@ -27,6 +27,8 @@ import androidx.annotation.IntDef; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.lang.annotation.Documented; @@ -823,6 +825,13 @@ public final class C { */ public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7; + /** + * The type of a message that can be passed to a {@link SimpleDecoderVideoRenderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link + * VideoDecoderOutputBufferRenderer}, or null. + */ + public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = 8; + /** * Applications or extensions may define custom {@code MSG_*} constants that can be passed to * {@link Renderer}s. These custom constants must be greater than or equal to this value. From 9cc927bc64eb8a507b4ee30d944f6686df574789 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 Sep 2019 16:04:42 +0100 Subject: [PATCH 481/807] Don't block on a dead thread when waiting for messages. PiperOrigin-RevId: 271983192 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 9d0692cf0e..664801ffd4 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 @@ -210,7 +210,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @Override public synchronized void sendMessage(PlayerMessage message) { - if (released) { + if (released || !internalPlaybackThread.isAlive()) { Log.w(TAG, "Ignoring messages sent after release."); message.markAsProcessed(/* isDelivered= */ false); return; @@ -219,6 +219,9 @@ import java.util.concurrent.atomic.AtomicBoolean; } public synchronized void setForegroundMode(boolean foregroundMode) { + if (released || !internalPlaybackThread.isAlive()) { + return; + } if (foregroundMode) { handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget(); } else { @@ -227,7 +230,7 @@ import java.util.concurrent.atomic.AtomicBoolean; .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag) .sendToTarget(); boolean wasInterrupted = false; - while (!processedFlag.get() && !released) { + while (!processedFlag.get()) { try { wait(); } catch (InterruptedException e) { @@ -242,7 +245,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } public synchronized void release() { - if (released) { + if (released || !internalPlaybackThread.isAlive()) { return; } handler.sendEmptyMessage(MSG_RELEASE); @@ -991,6 +994,11 @@ import java.util.concurrent.atomic.AtomicBoolean; private void sendMessageToTargetThread(final PlayerMessage message) { Handler handler = message.getHandler(); + if (!handler.getLooper().getThread().isAlive()) { + Log.w("TAG", "Trying to send message on a dead thread."); + message.markAsProcessed(/* isDelivered= */ false); + return; + } handler.post( () -> { try { From f0723a27b7a1aee99c03c1da9cba16f7bffeba6d Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 30 Sep 2019 16:07:44 +0100 Subject: [PATCH 482/807] Move HLS mapped track picking into a separate method This helps reduce the amount of nesting in HlsSampleStreamWrapper.track() PiperOrigin-RevId: 271983779 --- .../source/hls/HlsSampleStreamWrapper.java | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) 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 56e913b357..b87f83c336 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 @@ -814,24 +814,14 @@ import java.util.Set; @Override public TrackOutput track(int id, int type) { - int trackCount = sampleQueues.length; - if (MAPPABLE_TYPES.contains(type)) { // Track types in MAPPABLE_TYPES are handled manually to ignore IDs. - int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET); - if (sampleQueueIndex != C.INDEX_UNSET) { - if (sampleQueueMappingDoneByType.contains(type)) { - return sampleQueueTrackIds[sampleQueueIndex] == id - ? sampleQueues[sampleQueueIndex] - : createDummyTrackOutput(id, type); - } else { - sampleQueueMappingDoneByType.add(type); - sampleQueueTrackIds[sampleQueueIndex] = id; - return sampleQueues[sampleQueueIndex]; - } + @Nullable TrackOutput mappedTrackOutput = getMappedTrackOutput(id, type); + if (mappedTrackOutput != null) { + return mappedTrackOutput; } } else /* sparse track */ { - for (int i = 0; i < trackCount; i++) { + for (int i = 0; i < sampleQueues.length; i++) { if (sampleQueueTrackIds[i] == id) { return sampleQueues[i]; } @@ -840,6 +830,8 @@ import java.util.Set; if (tracksEnded) { return createDummyTrackOutput(id, type); } + + int trackCount = sampleQueues.length; SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); @@ -865,6 +857,37 @@ import java.util.Set; return trackOutput; } + /** + * Returns the {@link TrackOutput} for the provided {@code type} and {@code id}, or null if none + * has been created yet. + * + *

        If a {@link SampleQueue} for {@code type} has been created and is mapped, but it has a + * different ID, then return a {@link DummyTrackOutput} that does nothing. + * + *

        If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to + * this {@code id} and return it. This situation can happen after a call to {@link #init} with + * {@code reusingExtractor=false}. + * + * @param id The ID of the track. + * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}. + * @return The the mapped {@link TrackOutput}, or null if it's not been created yet. + */ + @Nullable + private TrackOutput getMappedTrackOutput(int id, int type) { + Assertions.checkArgument(MAPPABLE_TYPES.contains(type)); + int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET); + if (sampleQueueIndex == C.INDEX_UNSET) { + return null; + } + + if (sampleQueueMappingDoneByType.add(type)) { + sampleQueueTrackIds[sampleQueueIndex] = id; + } + return sampleQueueTrackIds[sampleQueueIndex] == id + ? sampleQueues[sampleQueueIndex] + : createDummyTrackOutput(id, type); + } + @Override public void endTracks() { tracksEnded = true; From 6a2d04e3ce8edae196ddacb998c62b541bbfe02b Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 Sep 2019 17:22:00 +0100 Subject: [PATCH 483/807] Reflect playback suppression in PlaybackStats states. Also split existing SUSPENDED state into ABANDONED and INTERUPTED_BY_AD for more clarity. PiperOrigin-RevId: 271997824 --- .../exoplayer2/analytics/PlaybackStats.java | 33 ++++-- .../analytics/PlaybackStatsListener.java | 104 +++++++++++++----- 2 files changed, 99 insertions(+), 38 deletions(-) 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 index bd8fb213ed..b370c893de 100644 --- 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 @@ -38,8 +38,10 @@ public final class PlaybackStats { * #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}. + * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link + * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link + * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link + * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -54,10 +56,13 @@ public final class PlaybackStats { PLAYBACK_STATE_BUFFERING, PLAYBACK_STATE_PAUSED_BUFFERING, PLAYBACK_STATE_SEEK_BUFFERING, + PLAYBACK_STATE_SUPPRESSED, + PLAYBACK_STATE_SUPPRESSED_BUFFERING, PLAYBACK_STATE_ENDED, PLAYBACK_STATE_STOPPED, PLAYBACK_STATE_FAILED, - PLAYBACK_STATE_SUSPENDED + PLAYBACK_STATE_INTERRUPTED_BY_AD, + PLAYBACK_STATE_ABANDONED }) @interface PlaybackState {} /** Playback has not started (initial state). */ @@ -72,22 +77,28 @@ public final class PlaybackStats { 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. */ + /** Playback is buffering to resume active 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 is suppressed (e.g. due to audio focus loss). */ + public static final int PLAYBACK_STATE_SUPPRESSED = 9; + /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */ + public static final int PLAYBACK_STATE_SUPPRESSED_BUFFERING = 10; /** 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; + public static final int PLAYBACK_STATE_ENDED = 11; + /** Playback is stopped and can be restarted. */ + public static final int PLAYBACK_STATE_STOPPED = 12; /** 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; + public static final int PLAYBACK_STATE_FAILED = 13; + /** Playback is interrupted by an ad. */ + public static final int PLAYBACK_STATE_INTERRUPTED_BY_AD = 14; + /** Playback is abandoned before reaching the end of the media. */ + public static final int PLAYBACK_STATE_ABANDONED = 15; /** Total number of playback states. */ - /* package */ static final int PLAYBACK_STATE_COUNT = 13; + /* package */ static final int PLAYBACK_STATE_COUNT = 16; /** Empty playback stats. */ public static final PlaybackStats EMPTY = merge(/* nothing */ ); 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 index 8b9f1d1ced..6768677fa4 100644 --- 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 @@ -81,6 +81,7 @@ public final class PlaybackStatsListener @Nullable private String activeAdPlayback; private boolean playWhenReady; @Player.State private int playbackState; + private boolean isSuppressed; private float playbackSpeed; /** @@ -205,7 +206,7 @@ public final class PlaybackStatsListener eventTime.currentPlaybackPositionMs, eventTime.totalBufferedDurationMs); Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) - .onSuspended(contentEventTime, /* belongsToPlayback= */ true); + .onInterruptedByAd(contentEventTime); } @Override @@ -222,7 +223,7 @@ public final class PlaybackStatsListener tracker.onPlayerStateChanged( eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); } - tracker.onSuspended(eventTime, /* belongsToPlayback= */ false); + tracker.onFinished(eventTime); PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); if (callback != null) { @@ -246,6 +247,19 @@ public final class PlaybackStatsListener } } + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, int playbackSuppressionReason) { + isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback); + } + } + @Override public void onTimelineChanged(EventTime eventTime, int reason) { sessionManager.handleTimelineUpdate(eventTime); @@ -456,9 +470,11 @@ public final class PlaybackStatsListener private long currentPlaybackStateStartTimeMs; private boolean isSeeking; private boolean isForeground; - private boolean isSuspended; + private boolean isInterruptedByAd; + private boolean isFinished; private boolean playWhenReady; @Player.State private int playerPlaybackState; + private boolean isSuppressed; private boolean hasFatalError; private boolean startedLoading; private long lastRebufferStartTimeMs; @@ -515,18 +531,32 @@ public final class PlaybackStatsListener hasFatalError = false; } if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { - isSuspended = false; + isInterruptedByAd = false; } maybeUpdatePlaybackState(eventTime, belongsToPlayback); } + /** + * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss), + * including all updates while the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param isSuppressed Whether playback is suppressed. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onIsSuppressedChanged( + EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) { + this.isSuppressed = isSuppressed; + 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; + isInterruptedByAd = false; maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); } @@ -561,7 +591,7 @@ public final class PlaybackStatsListener fatalErrorHistory.add(Pair.create(eventTime, error)); } hasFatalError = true; - isSuspended = false; + isInterruptedByAd = false; isSeeking = false; maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); } @@ -587,16 +617,24 @@ public final class PlaybackStatsListener } /** - * Notifies the tracker that the current playback has been suspended, e.g. for ad playback or - * permanently. + * Notifies the tracker that the current playback has been interrupted for ad playback. * * @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; + public void onInterruptedByAd(EventTime eventTime) { + isInterruptedByAd = true; isSeeking = false; - maybeUpdatePlaybackState(eventTime, belongsToPlayback); + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has finished. + * + * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback. + */ + public void onFinished(EventTime eventTime) { + isFinished = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false); } /** @@ -809,8 +847,9 @@ public final class PlaybackStatsListener rebufferCount++; lastRebufferStartTimeMs = eventTime.realtimeMs; } - if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING - && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_BUFFERING) { + if (isRebufferingState(currentPlaybackState) + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) { pauseBufferCount++; } @@ -829,11 +868,11 @@ public final class PlaybackStatsListener } private @PlaybackState int resolveNewPlaybackState() { - if (isSuspended) { + if (isFinished) { // 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; + : PlaybackStats.PLAYBACK_STATE_ABANDONED; } else if (isSeeking) { // Seeking takes precedence over errors such that we report a seek while in error state. return PlaybackStats.PLAYBACK_STATE_SEEKING; @@ -844,26 +883,34 @@ public final class PlaybackStatsListener return startedLoading ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + } else if (isInterruptedByAd) { + return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD; } 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) { + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { 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; + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING + : PlaybackStats.PLAYBACK_STATE_BUFFERING; } else if (playerPlaybackState == Player.STATE_READY) { - return playWhenReady - ? PlaybackStats.PLAYBACK_STATE_PLAYING - : PlaybackStats.PLAYBACK_STATE_PAUSED; + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED + : PlaybackStats.PLAYBACK_STATE_PLAYING; } 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 @@ -974,7 +1021,8 @@ public final class PlaybackStatsListener private static boolean isReadyState(@PlaybackState int state) { return state == PlaybackStats.PLAYBACK_STATE_PLAYING - || state == PlaybackStats.PLAYBACK_STATE_PAUSED; + || state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED; } private static boolean isPausedState(@PlaybackState int state) { @@ -984,21 +1032,23 @@ public final class PlaybackStatsListener private static boolean isRebufferingState(@PlaybackState int state) { return state == PlaybackStats.PLAYBACK_STATE_BUFFERING - || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_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) { + && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { return false; } return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND - && newState != PlaybackStats.PLAYBACK_STATE_SUSPENDED + && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD && newState != PlaybackStats.PLAYBACK_STATE_PLAYING && newState != PlaybackStats.PLAYBACK_STATE_PAUSED + && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED && newState != PlaybackStats.PLAYBACK_STATE_ENDED; } } From 26dd4aad68d90eb35a547fa4cb507128e287de6b Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 Sep 2019 17:23:29 +0100 Subject: [PATCH 484/807] Add missing methods to EventLogger. PiperOrigin-RevId: 271998087 --- .../android/exoplayer2/util/EventLogger.java | 61 ++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) 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 c37e98776e..ec9ddba122 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 @@ -23,9 +23,11 @@ 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.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; @@ -98,6 +100,20 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); } + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) { + logd( + eventTime, + "playbackSuppressionReason", + getPlaybackSuppressionReasonString(playbackSuppressionReason)); + } + + @Override + public void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) { + logd(eventTime, "isPlaying", Boolean.toString(isPlaying)); + } + @Override public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) { logd(eventTime, "repeatMode", getRepeatModeString(repeatMode)); @@ -134,7 +150,7 @@ public class EventLogger implements AnalyticsListener { int periodCount = eventTime.timeline.getPeriodCount(); int windowCount = eventTime.timeline.getWindowCount(); logd( - "timelineChanged [" + "timeline [" + getEventTimeString(eventTime) + ", periodCount=" + periodCount @@ -178,10 +194,10 @@ public class EventLogger implements AnalyticsListener { MappedTrackInfo mappedTrackInfo = trackSelector != null ? trackSelector.getCurrentMappedTrackInfo() : null; if (mappedTrackInfo == null) { - logd(eventTime, "tracksChanged", "[]"); + logd(eventTime, "tracks", "[]"); return; } - logd("tracksChanged [" + getEventTimeString(eventTime) + ", "); + logd("tracks [" + getEventTimeString(eventTime) + ", "); // Log tracks associated to renderers. int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { @@ -278,6 +294,25 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "audioSessionId", Integer.toString(audioSessionId)); } + @Override + public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) { + logd( + eventTime, + "audioAttributes", + audioAttributes.contentType + + "," + + audioAttributes.flags + + "," + + audioAttributes.usage + + "," + + audioAttributes.allowedCapturePolicy); + } + + @Override + public void onVolumeChanged(EventTime eventTime, float volume) { + logd(eventTime, "volume", Float.toString(volume)); + } + @Override public void onDecoderInitialized( EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { @@ -288,7 +323,7 @@ public class EventLogger implements AnalyticsListener { public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { logd( eventTime, - "decoderInputFormatChanged", + "decoderInputFormat", getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @@ -319,7 +354,7 @@ public class EventLogger implements AnalyticsListener { int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - logd(eventTime, "videoSizeChanged", width + ", " + height); + logd(eventTime, "videoSize", width + ", " + height); } @Override @@ -378,7 +413,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { - logd(eventTime, "surfaceSizeChanged", width + ", " + height); + logd(eventTime, "surfaceSize", width + ", " + height); } @Override @@ -388,7 +423,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { - logd(eventTime, "downstreamFormatChanged", Format.toLogString(mediaLoadData.trackFormat)); + logd(eventTime, "downstreamFormat", Format.toLogString(mediaLoadData.trackFormat)); } @Override @@ -625,4 +660,16 @@ public class EventLogger implements AnalyticsListener { return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; } } + + private static String getPlaybackSuppressionReasonString( + @PlaybackSuppressionReason int playbackSuppressionReason) { + switch (playbackSuppressionReason) { + case Player.PLAYBACK_SUPPRESSION_REASON_NONE: + return "NONE"; + case Player.PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS: + return "AUDIO_FOCUS_LOSS"; + default: + return "?"; + } + } } From dd4f9bcaae74cf50d9d10b8bd4d3cb729d7dc51d Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 Sep 2019 17:30:14 +0100 Subject: [PATCH 485/807] Add Timeline.Window.isLive This flag is currently merged into Window.isDynamic, which isn't always true because 1. A window can be dynamic for other reasons (e.g. when the duration is still missing). 2. A live stream can be become non-dynamic when it ends. Issue:#2668 Issue:#5973 PiperOrigin-RevId: 271999378 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/cast/CastTimeline.java | 1 + .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 3 +- .../google/android/exoplayer2/Timeline.java | 14 ++++- .../exoplayer2/source/MaskingMediaSource.java | 1 + .../source/ProgressiveMediaPeriod.java | 18 +++--- .../source/ProgressiveMediaSource.java | 18 ++++-- .../exoplayer2/source/SilenceMediaSource.java | 3 +- .../source/SinglePeriodTimeline.java | 17 +++++- .../source/SingleSampleMediaSource.java | 7 ++- .../exoplayer2/MediaPeriodQueueTest.java | 3 +- .../source/ClippingMediaSourceTest.java | 60 ++++++++++++++++--- .../source/SinglePeriodTimelineTest.java | 8 ++- .../source/dash/DashMediaSource.java | 15 +++-- .../exoplayer2/source/hls/HlsMediaSource.java | 2 + .../source/smoothstreaming/SsMediaSource.java | 5 +- .../exoplayer2/testutil/FakeTimeline.java | 1 + 17 files changed, 138 insertions(+), 41 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 349b904589..5743d9944c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -80,6 +80,9 @@ and move it to the core library. * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. +* Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). ### 2.10.5 (2019-09-20) ### 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 2857141f8f..54ff7e6777 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 @@ -121,6 +121,7 @@ import java.util.Arrays; /* windowStartTimeMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, + /* isLive= */ isDynamic, defaultPositionsUs[windowIndex], durationUs, /* firstPeriodIndex= */ windowIndex, 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 2995df4ab4..edaa4cde29 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 @@ -63,7 +63,8 @@ public class ImaAdsLoaderTest { private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = - new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + new SinglePeriodTimeline( + CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); private static final Uri TEST_URI = Uri.EMPTY; private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; 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 c496052f94..ce1a58822c 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 @@ -66,8 +66,9 @@ import com.google.android.exoplayer2.util.Assertions; * duration is unknown, since it's continually extending as more content is broadcast. If content * only remains available for a limited period of time then the window may start at a non-zero * position, defining the region of content that can still be played. The window will have {@link - * Window#isDynamic} set to true if the stream is still live. Its default position is typically near - * to the live edge (indicated by the black dot in the figure above). + * Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to + * true as long as we expect changes to the live window. Its default position is typically near to + * the live edge (indicated by the black dot in the figure above). * *

        Live stream with indefinite availability

        * @@ -158,8 +159,13 @@ public abstract class Timeline { public boolean isDynamic; /** - * The index of the first period that belongs to this window. + * Whether the media in this window is live. For informational purposes only. + * + *

        Check {@link #isDynamic} to know whether this window may still change. */ + public boolean isLive; + + /** The index of the first period that belongs to this window. */ public int firstPeriodIndex; /** @@ -200,6 +206,7 @@ public abstract class Timeline { long windowStartTimeMs, boolean isSeekable, boolean isDynamic, + boolean isLive, long defaultPositionUs, long durationUs, int firstPeriodIndex, @@ -212,6 +219,7 @@ public abstract class Timeline { this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.defaultPositionUs = defaultPositionUs; this.durationUs = durationUs; this.firstPeriodIndex = firstPeriodIndex; 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 index 8727fc5ed9..891cb351c1 100644 --- 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 @@ -317,6 +317,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { /* isSeekable= */ false, // Dynamic window to indicate pending timeline updates. /* isDynamic= */ true, + /* isLive= */ false, /* defaultPositionUs= */ 0, /* durationUs= */ C.TIME_UNSET, /* firstPeriodIndex= */ 0, 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 c768fe4981..4af5bafdda 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 @@ -74,13 +74,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; interface Listener { /** - * Called when the duration or ability to seek within the period changes. + * Called when the duration, the ability to seek within the period, or the categorization as + * live stream changes. * * @param durationUs The duration of the period, or {@link C#TIME_UNSET}. * @param isSeekable Whether the period is seekable. + * @param isLive Whether the period is live. */ - void onSourceInfoRefreshed(long durationUs, boolean isSeekable); - + void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive); } /** @@ -129,6 +130,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private int enabledTrackCount; private long durationUs; private long length; + private boolean isLive; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -551,7 +553,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - listener.onSourceInfoRefreshed(durationUs, isSeekable); + listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); } eventDispatcher.loadCompleted( loadable.dataSpec, @@ -740,14 +742,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } trackArray[i] = new TrackGroup(trackFormat); } - dataType = - length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET - ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE - : C.DATA_TYPE_MEDIA; + isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; + dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; preparedState = new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags); prepared = true; - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); Assertions.checkNotNull(callback).onPrepared(this); } 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 80bcdcd029..c88972da62 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 @@ -220,6 +220,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private long timelineDurationUs; private boolean timelineIsSeekable; + private boolean timelineIsLive; @Nullable private TransferListener transferListener; // TODO: Make private when ExtractorMediaSource is deleted. @@ -253,7 +254,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; drmSessionManager.prepare(); - notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive); } @Override @@ -293,27 +294,32 @@ public final class ProgressiveMediaSource extends BaseMediaSource // ProgressiveMediaPeriod.Listener implementation. @Override - public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { + public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { // If we already have the duration from a previous source info refresh, use it. durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; - if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) { + if (timelineDurationUs == durationUs + && timelineIsSeekable == isSeekable + && timelineIsLive == isLive) { // Suppress no-op source info changes. return; } - notifySourceInfoRefreshed(durationUs, isSeekable); + notifySourceInfoRefreshed(durationUs, isSeekable, isLive); } // Internal methods. - private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { + private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. + timelineIsLive = isLive; + // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then + // indicate that the duration may change until it's known. See [internal: b/69703223]. refreshSourceInfo( new SinglePeriodTimeline( timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, + /* isLive= */ timelineIsLive, /* manifest= */ null, tag)); } 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 index 3bb7ada7e0..a950a95457 100644 --- 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 @@ -68,7 +68,8 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false)); + new SinglePeriodTimeline( + durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false)); } @Override 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 49d67935a5..45f64cacf2 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,6 +35,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; + private final boolean isLive; @Nullable private final Object tag; @Nullable private final Object manifest; @@ -44,9 +45,11 @@ 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 isLive Whether the window is live. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, isSeekable, isDynamic, /* manifest= */ null, /* tag= */ null); + public SinglePeriodTimeline( + long durationUs, boolean isSeekable, boolean isDynamic, boolean isLive) { + this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null); } /** @@ -55,6 +58,7 @@ 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 isLive Whether the window is live. * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Window#tag}. */ @@ -62,6 +66,7 @@ public final class SinglePeriodTimeline extends Timeline { long durationUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this( @@ -71,6 +76,7 @@ public final class SinglePeriodTimeline extends Timeline { /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + isLive, manifest, tag); } @@ -87,6 +93,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 isLive Whether the window is live. * @param manifest The manifest. May be (@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ @@ -97,6 +104,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this( @@ -108,6 +116,7 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs, isSeekable, isDynamic, + isLive, manifest, tag); } @@ -127,6 +136,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 isLive Whether the window is live. * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ @@ -139,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; @@ -149,6 +160,7 @@ public final class SinglePeriodTimeline extends Timeline { this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.manifest = manifest; this.tag = tag; } @@ -182,6 +194,7 @@ public final class SinglePeriodTimeline extends Timeline { windowStartTimeMs, isSeekable, isDynamic, + isLive, windowDefaultStartPositionUs, windowDurationUs, 0, 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 04ee3a153c..be939fd018 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 @@ -291,7 +291,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); timeline = new SinglePeriodTimeline( - durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* manifest= */ null, tag); + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag); } // MediaSource implementation. 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 afcce904e9..1a0e13b6c1 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 @@ -44,7 +44,8 @@ public final class MediaPeriodQueueTest { private static final long SECOND_AD_START_TIME_US = 20 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = - new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + new SinglePeriodTimeline( + CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); private static final Uri AD_URI = Uri.EMPTY; private MediaPeriodQueue mediaPeriodQueue; 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 89acb3ec3e..532ad61b85 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 @@ -64,7 +64,12 @@ public final class ClippingMediaSourceTest { @Test public void testNoClipping() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -78,7 +83,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingUnseekableWindowThrows() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, false, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* isLive= */ false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -93,7 +103,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingStart() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -105,7 +120,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingEnd() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -121,7 +141,8 @@ public final class ClippingMediaSourceTest { // to it having loaded sufficient data to establish its duration and seekability. Such timelines // should not result in clipping failure. Timeline timeline = - new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true); + new SinglePeriodTimeline( + C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true, /* isLive= */ true); Timeline clippedTimeline = getClippedTimeline( @@ -139,7 +160,8 @@ public final class ClippingMediaSourceTest { new SinglePeriodTimeline( /* durationUs= */ TEST_PERIOD_DURATION_US, /* isSeekable= */ true, - /* isDynamic= */ false); + /* isDynamic= */ false, + /* isLive= */ false); // When clipping to the end, the clipped timeline should also have a duration. Timeline clippedTimeline = @@ -153,7 +175,10 @@ public final class ClippingMediaSourceTest { // Create a child timeline that has an unknown duration. Timeline timeline = new SinglePeriodTimeline( - /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ true, /* isDynamic= */ false); + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); // When clipping to the end, the clipped timeline should also have an unset duration. Timeline clippedTimeline = @@ -164,7 +189,12 @@ public final class ClippingMediaSourceTest { @Test public void testClippingStartAndEnd() throws IOException { - Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false); + Timeline timeline = + new SinglePeriodTimeline( + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); Timeline clippedTimeline = getClippedTimeline( @@ -185,6 +215,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -207,6 +238,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -217,6 +249,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -256,6 +289,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -266,6 +300,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -305,6 +340,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -315,6 +351,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -355,6 +392,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -365,6 +403,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -480,7 +519,10 @@ public final class ClippingMediaSourceTest { throws IOException { Timeline timeline = new SinglePeriodTimeline( - TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + TEST_PERIOD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline) { @Override 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 cb21db8212..6ff4f78fa2 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 @@ -41,7 +41,9 @@ public final class SinglePeriodTimelineTest { @Test public void testGetPeriodPositionDynamicWindowUnknownDuration() { - SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + SinglePeriodTimeline timeline = + new SinglePeriodTimeline( + C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true, /* isLive= */ true); // Should return null with any positive position projection. Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); assertThat(position).isNull(); @@ -62,6 +64,7 @@ public final class SinglePeriodTimelineTest { /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ false, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); // Should return null with a positive position projection beyond window duration. @@ -85,6 +88,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, /* tag= */ null); @@ -104,6 +108,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, tag); @@ -117,6 +122,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, /* tag= */ null); Object uid = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; 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 ee168f0458..352131d70a 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 @@ -1216,10 +1216,6 @@ public final class DashMediaSource extends BaseMediaSource { Assertions.checkIndex(windowIndex, 0, 1); long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); - boolean isDynamic = - manifest.dynamic - && manifest.minUpdatePeriodMs != C.TIME_UNSET - && manifest.durationMs == C.TIME_UNSET; return window.set( Window.SINGLE_WINDOW_UID, windowTag, @@ -1227,7 +1223,8 @@ public final class DashMediaSource extends BaseMediaSource { presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, - isDynamic, + /* isDynamic= */ isMovingLiveWindow(manifest), + /* isLive= */ manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, /* firstPeriodIndex= */ 0, @@ -1247,7 +1244,7 @@ public final class DashMediaSource extends BaseMediaSource { private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (!manifest.dynamic) { + if (!isMovingLiveWindow(manifest)) { return windowDefaultStartPositionUs; } if (defaultPositionProjectionUs > 0) { @@ -1292,6 +1289,12 @@ public final class DashMediaSource extends BaseMediaSource { Assertions.checkIndex(periodIndex, 0, getPeriodCount()); return firstPeriodId + periodIndex; } + + private static boolean isMovingLiveWindow(DashManifest manifest) { + return manifest.dynamic + && manifest.minUpdatePeriodMs != C.TIME_UNSET + && manifest.durationMs == C.TIME_UNSET; + } } private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { 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 1bda50ba88..f058ad5ba2 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 @@ -439,6 +439,7 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + /* isLive= */ true, manifest, tag); } else /* not live */ { @@ -455,6 +456,7 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ false, + /* isLive= */ false, manifest, tag); } 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 9ddb1e3932..4c05353186 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 @@ -696,7 +696,8 @@ public final class SsMediaSource extends BaseMediaSource /* windowPositionInPeriodUs= */ 0, /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, - manifest.isLive, + /* isDynamic= */ manifest.isLive, + /* isLive= */ manifest.isLive, manifest, tag); } else if (manifest.isLive) { @@ -719,6 +720,7 @@ public final class SsMediaSource extends BaseMediaSource defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, manifest, tag); } else { @@ -732,6 +734,7 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, /* isDynamic= */ false, + /* isLive= */ false, manifest, tag); } 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 af672f0da3..401fcf8034 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 @@ -189,6 +189,7 @@ public final class FakeTimeline extends Timeline { /* windowStartTimeMs= */ C.TIME_UNSET, windowDefinition.isSeekable, windowDefinition.isDynamic, + /* isLive= */ windowDefinition.isDynamic, /* defaultPositionUs= */ 0, windowDefinition.durationUs, periodOffsets[windowIndex], From 7d8bee799bd899b44b7457e3fed530c472426863 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 30 Sep 2019 18:01:22 +0100 Subject: [PATCH 486/807] Add convenience Player.isCurrentWindowLive method. PiperOrigin-RevId: 272005632 --- .../java/com/google/android/exoplayer2/BasePlayer.java | 6 ++++++ .../main/java/com/google/android/exoplayer2/Player.java | 7 +++++++ 2 files changed, 13 insertions(+) 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 baa2a767b5..2646cbc035 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 @@ -127,6 +127,12 @@ public abstract class BasePlayer implements Player { return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; } + @Override + public final boolean isCurrentWindowLive() { + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive; + } + @Override public final boolean isCurrentWindowSeekable() { Timeline timeline = getCurrentTimeline(); 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 d809dcbc88..5f00916892 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 @@ -961,6 +961,13 @@ public interface Player { */ boolean isCurrentWindowDynamic(); + /** + * Returns whether the current window is live, or {@code false} if the {@link Timeline} is empty. + * + * @see Timeline.Window#isLive + */ + boolean isCurrentWindowLive(); + /** * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is * empty. From c9029479f4023875a58b57b2f46aa5bfd04f0ba7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 30 Sep 2019 18:21:04 +0100 Subject: [PATCH 487/807] Remove references to cast_receiver_app PiperOrigin-RevId: 272010353 --- .gitignore | 4 ---- .hgignore | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.gitignore b/.gitignore index 4731d5ba99..2ec73a6fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,3 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md - -# Cast receiver -cast_receiver_app/external-js -cast_receiver_app/bazel-cast_receiver_app diff --git a/.hgignore b/.hgignore index 36d3268005..e8a0722d03 100644 --- a/.hgignore +++ b/.hgignore @@ -75,7 +75,3 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md - -# Cast receiver -cast_receiver_app/external-js -cast_receiver_app/bazel-cast_receiver_app From af9ce21e2f385b390cb291ee0333b5f10e35fcb1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 1 Oct 2019 10:26:52 +0100 Subject: [PATCH 488/807] Log failures when instantiating Framework MediaDrms PiperOrigin-RevId: 272166041 --- .../com/google/android/exoplayer2/drm/FrameworkMediaDrm.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 42050d7eb9..f0527eac0e 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 @@ -48,6 +48,8 @@ import java.util.UUID; @TargetApi(23) public final class FrameworkMediaDrm implements ExoMediaDrm { + private static final String TAG = "FrameworkMediaDrm"; + /** * {@link ExoMediaDrm.Provider} that returns a new {@link FrameworkMediaDrm} for the requested * UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID @@ -60,6 +62,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm(); } }; @@ -68,7 +71,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm"; private static final int UTF_16_BYTES_PER_CHARACTER = 2; - private static final String TAG = "FrameworkMediaDrm"; private final UUID uuid; private final MediaDrm mediaDrm; From 6c20616f87d8528d998b7120499f467a2235ac70 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 1 Oct 2019 11:56:39 +0100 Subject: [PATCH 489/807] remove validation comments from github templates PiperOrigin-RevId: 272176894 --- .github/ISSUE_TEMPLATE/bug.md | 5 ----- .github/ISSUE_TEMPLATE/content_not_playing.md | 5 ----- .github/ISSUE_TEMPLATE/feature_request.md | 5 ----- .github/ISSUE_TEMPLATE/question.md | 5 ----- 4 files changed, 20 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index c0980df440..8824c9e8d8 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -55,8 +55,3 @@ Specify the absolute version number. Avoid using terms such as "latest". Specify the devices and versions of Android on which the issue can be reproduced, and how easily it reproduces. If possible, please test on multiple devices and Android versions. - - diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md index c8d4668a6a..91ddf725ce 100644 --- a/.github/ISSUE_TEMPLATE/content_not_playing.md +++ b/.github/ISSUE_TEMPLATE/content_not_playing.md @@ -51,8 +51,3 @@ 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/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index d481de33ce..d660d0342a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -28,8 +28,3 @@ A clear and concise description of your proposed solution, if you have one. ### Alternatives considered A clear and concise description of any alternative solutions you considered, if applicable. - - diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index b5f40884d8..f3ad83b67d 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -48,8 +48,3 @@ dev.exoplayer@gmail.com using a subject 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. - - From f5873b8f00d339c9c8dabc6cdaeb4a58d8486010 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Tue, 1 Oct 2019 11:57:31 +0100 Subject: [PATCH 490/807] WakeLock handling Created the WakeLockManager for use in SimpleExoPlayer. Added a setter in SimpleExoPlayer to adjust this functionality. Issue:#5846 PiperOrigin-RevId: 272176998 --- RELEASENOTES.md | 4 + .../android/exoplayer2/SimpleExoPlayer.java | 33 ++++++ .../android/exoplayer2/WakeLockManager.java | 102 ++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5743d9944c..a84f4ff0b7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -72,6 +72,10 @@ ([#6161](https://github.com/google/ExoPlayer/issues/6161)). * Add demo app to show how to use the Android 10 `SurfaceControl` API with ExoPlayer ([#677](https://github.com/google/ExoPlayer/issues/677)). +* Add automatic `WakeLock` handling to `SimpleExoPlayer` through calling + `setEnableWakeLock`, which requires the + `android.Manifest.permission#WAKE_LOCK` permission + ([#5846](https://github.com/google/ExoPlayer/issues/5846)). * Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to detect playbacks suppressions (e.g. audio focus loss) directly ([#6203](https://github.com/google/ExoPlayer/issues/6203)). 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 43a5ebab99..418aa366e6 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 @@ -323,6 +323,7 @@ public class SimpleExoPlayer extends BasePlayer private final AnalyticsCollector analyticsCollector; private final AudioFocusManager audioFocusManager; + private final WakeLockManager wakeLockManager; @Nullable private Format videoFormat; @Nullable private Format audioFormat; @@ -453,6 +454,7 @@ public class SimpleExoPlayer extends BasePlayer ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); } audioFocusManager = new AudioFocusManager(context, componentListener); + wakeLockManager = new WakeLockManager(context); } @Override @@ -1226,6 +1228,7 @@ public class SimpleExoPlayer extends BasePlayer public void release() { verifyApplicationThread(); audioFocusManager.handleStop(); + wakeLockManager.setStayAwake(false); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -1348,6 +1351,22 @@ public class SimpleExoPlayer extends BasePlayer return player.getContentBufferedPosition(); } + /** + * Sets whether to enable the acquiring and releasing of a {@link + * android.os.PowerManager.WakeLock}. + * + *

        By default, automatic wake lock handling is not enabled. Enabling this on will acquire the + * WakeLock if necessary. Disabling this will release the WakeLock if it is held. + * + * @param handleWakeLock True if the player should handle a {@link + * android.os.PowerManager.WakeLock}, false otherwise. This is for use with a foreground + * {@link android.app.Service}, for allowing audio playback with the screen off. Please note + * that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + */ + public void setHandleWakeLock(boolean handleWakeLock) { + wakeLockManager.setEnabled(handleWakeLock); + } + // Internal methods. private void removeSurfaceCallbacks() { @@ -1667,5 +1686,19 @@ public class SimpleExoPlayer extends BasePlayer } } } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { + switch (playbackState) { + case Player.STATE_READY: + case Player.STATE_BUFFERING: + wakeLockManager.setStayAwake(playWhenReady); + break; + case Player.STATE_ENDED: + case Player.STATE_IDLE: + wakeLockManager.setStayAwake(false); + break; + } + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java new file mode 100644 index 0000000000..58c3748f39 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java @@ -0,0 +1,102 @@ +/* + * 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; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WakeLock}. + * + *

        The handling of wake locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +public final class WakeLockManager { + + private static final String TAG = "WakeLockManager"; + private static final String WAKE_LOCK_TAG = "ExoPlayer:WakeLockManager"; + + @Nullable private final PowerManager powerManager; + @Nullable private WakeLock wakeLock; + private boolean enabled; + private boolean stayAwake; + + public WakeLockManager(Context context) { + powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + } + + /** + * Sets whether to enable the acquiring and releasing of the {@link WakeLock}. + * + *

        By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if + * necessary. Disabling this will release the wake lock if it is held. + * + * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. Please + * note that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ + public void setEnabled(boolean enabled) { + if (enabled) { + if (wakeLock == null) { + if (powerManager == null) { + Log.w(TAG, "PowerManager was null, therefore the WakeLock was not created."); + return; + } + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + } + } + + this.enabled = enabled; + updateWakeLock(); + } + + /** + * Sets whether to acquire or release the {@link WakeLock}. + * + *

        Please note this method requires wake lock handling to be enabled through setEnabled(boolean + * enable) to actually have an impact on the {@link WakeLock}. + * + * @param stayAwake True if the player should acquire the {@link WakeLock}. False if the player + * should release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWakeLock(); + } + + // WakelockTimeout suppressed because the time the wake lock is needed for is unknown (could be + // listening to radio with screen off for multiple hours), therefore we can not determine a + // reasonable timeout that would not affect the user. + @SuppressLint("WakelockTimeout") + private void updateWakeLock() { + // Needed for the library nullness check. If enabled is true, the wakelock will not be null. + if (wakeLock != null) { + if (enabled) { + if (stayAwake && !wakeLock.isHeld()) { + wakeLock.acquire(); + } else if (!stayAwake && wakeLock.isHeld()) { + wakeLock.release(); + } + } else if (wakeLock.isHeld()) { + wakeLock.release(); + } + } + } +} From db68aa94901f32f34d415b559f932e86b9b98f6b Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 2 Oct 2019 11:50:12 +0100 Subject: [PATCH 491/807] Rollback of https://github.com/google/ExoPlayer/commit/01f484cbe965f7858f9621c97fec94d30eef188a *** Original commit *** Modify EventMessageDecoder to return null if decoding fails (currently throws exceptions) This matches the documentation on MetadataDecoder.decode: "@return The decoded metadata object, or null if the metadata could not be decoded." *** PiperOrigin-RevId: 272405287 --- .../metadata/emsg/EventMessageDecoder.java | 28 ++++++------------- .../metadata/MetadataRendererTest.java | 22 +++++---------- .../source/dash/PlayerEmsgHandler.java | 3 -- 3 files changed, 15 insertions(+), 38 deletions(-) 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 bbf7476d25..d87376feb0 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -29,31 +28,20 @@ public final class EventMessageDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override - @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); byte[] data = buffer.array(); int size = buffer.limit(); - EventMessage decodedEventMessage = decode(new ParsableByteArray(data, size)); - if (decodedEventMessage == null) { - return null; - } else { - return new Metadata(decodedEventMessage); - } + return new Metadata(decode(new ParsableByteArray(data, size))); } - @Nullable public EventMessage decode(ParsableByteArray emsgData) { - try { - String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long durationMs = emsgData.readUnsignedInt(); - long id = emsgData.readUnsignedInt(); - byte[] messageData = - Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); - return new EventMessage(schemeIdUri, value, durationMs, id, messageData); - } catch (RuntimeException e) { - return null; - } + String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + long durationMs = emsgData.readUnsignedInt(); + long id = emsgData.readUnsignedInt(); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index de18d370ec..1ad0ce6b79 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -78,20 +78,13 @@ public class MetadataRendererTest { /* id= */ 0, "Test data".getBytes(UTF_8)); - List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); assertThat(metadata.get(0).get(0)).isEqualTo(emsg); } - @Test - public void decodeMetadata_skipsMalformed() throws Exception { - List metadata = runRenderer(EMSG_FORMAT, "not valid emsg bytes".getBytes(UTF_8)); - - assertThat(metadata).isEmpty(); - } - @Test public void decodeMetadata_handlesId3WrappedInEmsg() throws Exception { EventMessage emsg = @@ -102,7 +95,7 @@ public class MetadataRendererTest { /* id= */ 0, encodeTxxxId3Frame("Test description", "Test value")); - List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); @@ -122,7 +115,7 @@ public class MetadataRendererTest { /* id= */ 0, SCTE35_TIME_SIGNAL_BYTES); - List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); @@ -139,18 +132,17 @@ public class MetadataRendererTest { /* id= */ 0, "Not a real ID3 tag".getBytes(ISO_8859_1)); - List metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg)); + List metadata = runRenderer(eventMessageEncoder.encode(emsg)); assertThat(metadata).isEmpty(); } - private static List runRenderer(Format format, byte[] input) - throws ExoPlaybackException { + private static List runRenderer(byte[] input) throws ExoPlaybackException { List metadata = new ArrayList<>(); MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null); renderer.replaceStream( - new Format[] {format}, - new FakeSampleStream(format, /* eventDispatcher= */ null, input), + new Format[] {EMSG_FORMAT}, + new FakeSampleStream(EMSG_FORMAT, /* eventDispatcher= */ null, input), /* offsetUs= */ 0L); renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data 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 883ca7420e..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 @@ -360,9 +360,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { } long eventTimeUs = inputBuffer.timeUs; Metadata metadata = decoder.decode(inputBuffer); - if (metadata == null) { - continue; - } EventMessage eventMessage = (EventMessage) metadata.get(0); if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { parsePlayerEmsgEvent(eventTimeUs, eventMessage); From 6780b802e0b6e613f242ef165d4beb907b192481 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 2 Oct 2019 11:50:35 +0100 Subject: [PATCH 492/807] Pass the raw ICY metadata through IcyInfo The ICY 'spec' isn't really clear/tight enough to do anything more specific than this I think. Issue:#6476 PiperOrigin-RevId: 272405322 --- RELEASENOTES.md | 2 + .../exoplayer2/metadata/icy/IcyDecoder.java | 9 +--- .../exoplayer2/metadata/icy/IcyInfo.java | 26 ++++++---- .../metadata/icy/IcyDecoderTest.java | 48 +++++++++++++++---- ...cyStreamInfoTest.java => IcyInfoTest.java} | 4 +- 5 files changed, 62 insertions(+), 27 deletions(-) rename library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/{IcyStreamInfoTest.java => IcyInfoTest.java} (91%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a84f4ff0b7..50418ded28 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -87,6 +87,8 @@ * Add `Timeline.Window.isLive` to indicate that a window is a live stream ([#2668](https://github.com/google/ExoPlayer/issues/2668) and [#5973](https://github.com/google/ExoPlayer/issues/5973)). +* Expose the raw ICY metadata through `IcyInfo` + ([#6476](https://github.com/google/ExoPlayer/issues/6476)). ### 2.10.5 (2019-09-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 12f65f1cda..13d6b485b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -15,13 +15,11 @@ */ package com.google.android.exoplayer2.metadata.icy; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.regex.Matcher; @@ -37,7 +35,6 @@ public final class IcyDecoder implements MetadataDecoder { private static final String STREAM_KEY_URL = "streamurl"; @Override - @Nullable @SuppressWarnings("ByteBufferBackingArray") public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); @@ -46,7 +43,6 @@ public final class IcyDecoder implements MetadataDecoder { return decode(Util.fromUtf8Bytes(data, 0, length)); } - @Nullable @VisibleForTesting /* package */ Metadata decode(String metadata) { String name = null; @@ -63,12 +59,9 @@ public final class IcyDecoder implements MetadataDecoder { case STREAM_KEY_URL: url = value; break; - default: - Log.w(TAG, "Unrecognized ICY tag: " + name); - break; } index = matcher.end(); } - return (name != null || url != null) ? new Metadata(new IcyInfo(name, url)) : null; + return new Metadata(new IcyInfo(metadata, name, url)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java index e6b915a6c8..717bb2b2e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java @@ -19,26 +19,35 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; /** ICY in-stream information. */ public final class IcyInfo implements Metadata.Entry { + /** The complete metadata string used to construct this IcyInfo. */ + public final String rawMetadata; /** The stream title if present, or {@code null}. */ @Nullable public final String title; - /** The stream title if present, or {@code null}. */ + /** The stream URL if present, or {@code null}. */ @Nullable public final String url; /** + * Construct a new IcyInfo from the source metadata string, and optionally a StreamTitle & + * StreamUrl that have been extracted. + * + * @param rawMetadata See {@link #rawMetadata}. * @param title See {@link #title}. * @param url See {@link #url}. */ - public IcyInfo(@Nullable String title, @Nullable String url) { + public IcyInfo(String rawMetadata, @Nullable String title, @Nullable String url) { + this.rawMetadata = rawMetadata; this.title = title; this.url = url; } /* package */ IcyInfo(Parcel in) { + rawMetadata = Assertions.checkNotNull(in.readString()); title = in.readString(); url = in.readString(); } @@ -52,26 +61,27 @@ public final class IcyInfo implements Metadata.Entry { return false; } IcyInfo other = (IcyInfo) obj; - return Util.areEqual(title, other.title) && Util.areEqual(url, other.url); + // title & url are derived from rawMetadata, so no need to include them in the comparison. + return Util.areEqual(rawMetadata, other.rawMetadata); } @Override public int hashCode() { - int result = 17; - result = 31 * result + (title != null ? title.hashCode() : 0); - result = 31 * result + (url != null ? url.hashCode() : 0); - return result; + // title & url are derived from rawMetadata, so no need to include them in the hash. + return rawMetadata.hashCode(); } @Override public String toString() { - return "ICY: title=\"" + title + "\", url=\"" + url + "\""; + return String.format( + "ICY: title=\"%s\", url=\"%s\", rawMetadata=\"%s\"", title, url, rawMetadata); } // Parcelable implementation. @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(rawMetadata); dest.writeString(title); dest.writeString(url); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java index 4602d172a6..72237d665c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.java @@ -29,10 +29,12 @@ public final class IcyDecoderTest { @Test public void decode() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test title';StreamURL='test_url';"); + String icyContent = "StreamTitle='test title';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test title"); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -40,21 +42,39 @@ public final class IcyDecoderTest { @Test public void decode_titleOnly() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test title';"); + String icyContent = "StreamTitle='test title';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test title"); assertThat(streamInfo.url).isNull(); } @Test - public void decode_emptyTitle() { + public void decode_extraTags() { + String icyContent = + "StreamTitle='test title';StreamURL='test_url';CustomTag|withWeirdSeparator"; IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); + assertThat(streamInfo.title).isEqualTo("test title"); + assertThat(streamInfo.url).isEqualTo("test_url"); + } + + @Test + public void decode_emptyTitle() { + IcyDecoder decoder = new IcyDecoder(); + String icyContent = "StreamTitle='';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); + + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEmpty(); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -62,10 +82,12 @@ public final class IcyDecoderTest { @Test public void decode_semiColonInTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test; title';StreamURL='test_url';"); + String icyContent = "StreamTitle='test; title';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test; title"); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -73,10 +95,12 @@ public final class IcyDecoderTest { @Test public void decode_quoteInTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test' title';StreamURL='test_url';"); + String icyContent = "StreamTitle='test' title';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test' title"); assertThat(streamInfo.url).isEqualTo("test_url"); } @@ -84,19 +108,25 @@ public final class IcyDecoderTest { @Test public void decode_lineTerminatorInTitle() { IcyDecoder decoder = new IcyDecoder(); - Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';"); + String icyContent = "StreamTitle='test\r\ntitle';StreamURL='test_url';"; + Metadata metadata = decoder.decode(icyContent); assertThat(metadata.length()).isEqualTo(1); IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo(icyContent); assertThat(streamInfo.title).isEqualTo("test\r\ntitle"); assertThat(streamInfo.url).isEqualTo("test_url"); } @Test - public void decode_notIcy() { + public void decode_noReconisedHeaders() { IcyDecoder decoder = new IcyDecoder(); Metadata metadata = decoder.decode("NotIcyData"); - assertThat(metadata).isNull(); + assertThat(metadata.length()).isEqualTo(1); + IcyInfo streamInfo = (IcyInfo) metadata.get(0); + assertThat(streamInfo.rawMetadata).isEqualTo("NotIcyData"); + assertThat(streamInfo.title).isNull(); + assertThat(streamInfo.url).isNull(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyStreamInfoTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java similarity index 91% rename from library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyStreamInfoTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java index 2bffe171d3..2c8e6616c9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyStreamInfoTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.java @@ -24,11 +24,11 @@ import org.junit.runner.RunWith; /** Test for {@link IcyInfo}. */ @RunWith(AndroidJUnit4.class) -public final class IcyStreamInfoTest { +public final class IcyInfoTest { @Test public void parcelEquals() { - IcyInfo streamInfo = new IcyInfo("name", "url"); + IcyInfo streamInfo = new IcyInfo("StreamName='name';StreamUrl='url'", "name", "url"); // Write to parcel. Parcel parcel = Parcel.obtain(); streamInfo.writeToParcel(parcel, 0); From f7b8d07cd2faac1dbe97a6c294548ca782ec6dd4 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 2 Oct 2019 11:53:03 +0100 Subject: [PATCH 493/807] Add specific error message if file-not-found for path with fragment or query This doesn't change the current behaviour, just adds a clear error message to the developer with instructions on how to avoid it. Issue:#6470 PiperOrigin-RevId: 272405556 --- RELEASENOTES.md | 2 ++ .../exoplayer2/upstream/FileDataSource.java | 26 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50418ded28..83997fe3c7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -89,6 +89,8 @@ [#5973](https://github.com/google/ExoPlayer/issues/5973)). * Expose the raw ICY metadata through `IcyInfo` ([#6476](https://github.com/google/ExoPlayer/issues/6476)). +* Fail more explicitly when local-file Uris contain invalid parts (e.g. + fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). ### 2.10.5 (2019-09-20) ### 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 e329dc722e..38b4a1da03 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 @@ -18,10 +18,12 @@ package com.google.android.exoplayer2.upstream; import static com.google.android.exoplayer2.util.Util.castNonNull; import android.net.Uri; +import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; @@ -37,6 +39,9 @@ public final class FileDataSource extends BaseDataSource { super(cause); } + public FileDataSourceException(String message, IOException cause) { + super(message, cause); + } } @Nullable private RandomAccessFile file; @@ -55,8 +60,8 @@ public final class FileDataSource extends BaseDataSource { this.uri = uri; transferInitializing(dataSpec); - RandomAccessFile file = new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); - this.file = file; + + this.file = openLocalFile(uri); file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position @@ -74,6 +79,23 @@ public final class FileDataSource extends BaseDataSource { return bytesRemaining; } + private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { + try { + return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + } catch (FileNotFoundException e) { + if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { + throw new FileDataSourceException( + String.format( + "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" + + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" + + " avoid this. path=%s,query=%s,fragment=%s", + uri.getPath(), uri.getQuery(), uri.getFragment()), + e); + } + throw new FileDataSourceException(e); + } + } + @Override public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException { if (readLength == 0) { From bc9a0860e0119957ecbe970fb62554b143b31a7f Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 2 Oct 2019 15:40:58 +0100 Subject: [PATCH 494/807] Clear renderedFirstFrame at stream changes even if no reconfiguration is needed This ensures a more consistent playback behavior no matter if an item is the first playlist item or a later one. For example, each playlist item gets its own onRenderedFirstFrame callback and other logic within the renderer that force renders the first frame more quickly is also triggered. PiperOrigin-RevId: 272434814 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 035e3bfad8..c66ce82614 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 @@ -802,9 +802,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs; boolean isStarted = getState() == STATE_STARTED; - // Don't force output until we joined and always render first frame if not joining. + // Don't force output until we joined and the position reached the current stream. boolean forceRenderOutputBuffer = joiningDeadlineMs == C.TIME_UNSET + && positionUs >= outputStreamOffsetUs && (!renderedFirstFrame || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))); if (forceRenderOutputBuffer) { @@ -956,6 +957,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { pendingOutputStreamSwitchTimesUs, /* destPos= */ 0, pendingOutputStreamOffsetCount); + clearRenderedFirstFrame(); } } From 9f78187678a949c84da94a3b82947bd259820b1c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 2 Oct 2019 16:40:01 +0100 Subject: [PATCH 495/807] Migrate Cast demo app to use DefaultDrmSessionManager.Builder PiperOrigin-RevId: 272444896 --- .../exoplayer2/castdemo/PlayerManager.java | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index b1a12f6bc9..894012664c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -29,10 +29,9 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; 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.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter; import com.google.android.exoplayer2.ext.cast.MediaItem; @@ -54,7 +53,6 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; -import java.util.IdentityHashMap; import java.util.Map; /** Manages players and an internal media queue for the demo app. */ @@ -87,7 +85,6 @@ import java.util.Map; private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; private final MediaItemConverter mediaItemConverter; - private final IdentityHashMap mediaDrms; private TrackGroupArray lastSeenTrackGroupArray; private int currentItemIndex; @@ -115,7 +112,6 @@ import java.util.Map; currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); mediaItemConverter = new DefaultMediaItemConverter(); - mediaDrms = new IdentityHashMap<>(); trackSelector = new DefaultTrackSelector(context); exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); @@ -185,8 +181,7 @@ import java.util.Map; if (itemIndex == -1) { return false; } - MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex); - releaseMediaDrmOfMediaSource(removedMediaSource); + concatenatingMediaSource.removeMediaSource(itemIndex); if (currentPlayer == castPlayer) { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); @@ -262,9 +257,6 @@ import java.util.Map; currentItemIndex = C.INDEX_UNSET; mediaQueue.clear(); concatenatingMediaSource.clear(); - for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) { - mediaDrm.release(); - } castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); localPlayerView.setPlayer(null); @@ -413,8 +405,7 @@ import java.util.Map; throw new IllegalArgumentException("mimeType is required"); } - FrameworkMediaDrm mediaDrm = null; - DrmSessionManager drmSessionManager = + DrmSessionManager drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; if (drmConfiguration != null) { @@ -425,18 +416,12 @@ import java.util.Map; for (Map.Entry requestHeader : drmConfiguration.requestHeaders.entrySet()) { drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue()); } - try { - mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid); - drmSessionManager = - new DefaultDrmSessionManager<>( - drmConfiguration.uuid, - mediaDrm, - drmCallback, - /* optionalKeyRequestParameters= */ null, - /* multiSession= */ true); - } catch (UnsupportedDrmException e) { - // Do nothing. The track selector will avoid selecting the DRM protected tracks. - } + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setMultiSession(/* multiSession= */ true) + .setUuidAndExoMediaDrmProvider( + drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); } MediaSource createdMediaSource; @@ -468,16 +453,6 @@ import java.util.Map; default: throw new IllegalArgumentException("mimeType is unsupported: " + mimeType); } - if (mediaDrm != null) { - mediaDrms.put(createdMediaSource, mediaDrm); - } return createdMediaSource; } - - private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) { - FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource); - if (mediaDrmToRelease != null) { - mediaDrmToRelease.release(); - } - } } From 4f640bc62e713ba13ff97fdc543824665156a17a Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 2 Oct 2019 16:43:26 +0100 Subject: [PATCH 496/807] Add SequencableLoader.isLoading This method allows the player to figure out whether we still have an ongoing load even if LoadControl.shouldContinueLoading returns false. PiperOrigin-RevId: 272445577 --- RELEASENOTES.md | 1 + .../android/exoplayer2/source/ClippingMediaPeriod.java | 5 +++++ .../exoplayer2/source/CompositeSequenceableLoader.java | 9 +++++++++ .../android/exoplayer2/source/MaskingMediaPeriod.java | 5 +++++ .../google/android/exoplayer2/source/MediaPeriod.java | 3 +++ .../android/exoplayer2/source/MergingMediaPeriod.java | 5 +++++ .../exoplayer2/source/ProgressiveMediaPeriod.java | 5 +++++ .../android/exoplayer2/source/SequenceableLoader.java | 3 +++ .../android/exoplayer2/source/SilenceMediaSource.java | 5 +++++ .../exoplayer2/source/SingleSampleMediaPeriod.java | 5 +++++ .../exoplayer2/source/chunk/ChunkSampleStream.java | 5 +++++ .../android/exoplayer2/util/ConditionVariable.java | 4 ++++ .../source/CompositeSequenceableLoaderTest.java | 5 +++++ .../android/exoplayer2/source/dash/DashMediaPeriod.java | 5 +++++ .../android/exoplayer2/source/hls/HlsMediaPeriod.java | 5 +++++ .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 5 +++++ .../exoplayer2/source/smoothstreaming/SsMediaPeriod.java | 5 +++++ .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 5 +++++ .../android/exoplayer2/testutil/FakeMediaPeriod.java | 5 +++++ 19 files changed, 90 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 83997fe3c7..2c46c34d21 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -91,6 +91,7 @@ ([#6476](https://github.com/google/ExoPlayer/issues/6476)). * Fail more explicitly when local-file Uris contain invalid parts (e.g. fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). +* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. ### 2.10.5 (2019-09-20) ### 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 d57dccd8fe..8aafb9a0e5 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 @@ -211,6 +211,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return mediaPeriod.isLoading(); + } + // MediaPeriod.Callback implementation. @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index c41933b48b..b583705170 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -83,4 +83,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader { return madeProgress; } + @Override + public boolean isLoading() { + for (SequenceableLoader loader : loaders) { + if (loader.isLoading()) { + return true; + } + } + return false; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java index 344c4989eb..17ac6c0667 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java @@ -211,6 +211,11 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return mediaPeriod != null && mediaPeriod.isLoading(); + } + @Override public void onContinueLoadingRequested(MediaPeriod source) { castNonNull(callback).onContinueLoadingRequested(this); 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 3f306c0c8a..2e2cf9caba 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 @@ -231,6 +231,9 @@ public interface MediaPeriod extends SequenceableLoader { @Override boolean continueLoading(long positionUs); + /** Returns whether the media period is currently loading. */ + boolean isLoading(); + /** * Re-evaluates the buffer given the playback position. * 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 cafc052f34..afa25d6fce 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 @@ -169,6 +169,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); 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 4af5bafdda..7ffc0faee6 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 @@ -358,6 +358,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return continuedLoading; } + @Override + public boolean isLoading() { + return loader.isLoading() && loadCondition.isOpen(); + } + @Override public long getNextLoadPositionUs() { return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 182f0f17cc..189c13ef0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -60,6 +60,9 @@ public interface SequenceableLoader { */ boolean continueLoading(long positionUs); + /** Returns whether the loader is currently loading. */ + boolean isLoading(); + /** * Re-evaluates the buffer given the playback position. * 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 index a950a95457..abaf33633e 100644 --- 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 @@ -172,6 +172,11 @@ public final class SilenceMediaSource extends BaseMediaSource { return false; } + @Override + public boolean isLoading() { + return false; + } + @Override public void reevaluateBuffer(long positionUs) {} 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 8d41fbc73f..a5d8266ef6 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 @@ -172,6 +172,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public long readDiscontinuity() { if (!notifiedReadingStarted) { 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 6817f294b2..61e2868725 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 @@ -577,6 +577,11 @@ public class ChunkSampleStream implements SampleStream, S return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public long getNextLoadPositionUs() { if (isPendingReset()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 058a5d6dd2..c035c62a7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -76,4 +76,8 @@ public final class ConditionVariable { return isOpen; } + /** Returns whether the condition is opened. */ + public synchronized boolean isOpen() { + return isOpen; + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java index 4a881d53b4..c996aadddb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -261,6 +261,11 @@ public final class CompositeSequenceableLoaderTest { return loaded; } + @Override + public boolean isLoading() { + return nextChunkDurationUs != 0; + } + @Override public void reevaluateBuffer(long positionUs) { // Do nothing. 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 d62474cb1b..bb8226e172 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 @@ -297,6 +297,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return compositeSequenceableLoader.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); 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 7d47cc43c6..b0b4c04b48 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 @@ -359,6 +359,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); 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 b87f83c336..58c275664b 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 @@ -663,6 +663,11 @@ import java.util.Set; return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public void reevaluateBuffer(long positionUs) { // Do nothing. 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 b3d950959a..42ac82e553 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 @@ -184,6 +184,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return compositeSequenceableLoader.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); 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 6d141fe04a..9c6fdc85cd 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 @@ -138,6 +138,11 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod return sequenceableLoader.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return sequenceableLoader.isLoading(); + } + @Override protected SampleStream createSampleStream(TrackSelection trackSelection) { FakeChunkSource chunkSource = diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d524d381fa..bcc96ef47e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -217,6 +217,11 @@ public class FakeMediaPeriod implements MediaPeriod { return false; } + @Override + public boolean isLoading() { + return false; + } + protected SampleStream createSampleStream(TrackSelection selection) { return new FakeSampleStream( selection.getSelectedFormat(), eventDispatcher, /* shouldOutputSample= */ true); From 03ba1c9a2fdc22f1b129288af53758429c93b5d0 Mon Sep 17 00:00:00 2001 From: wingyippp Date: Thu, 3 Oct 2019 13:50:17 +0800 Subject: [PATCH 497/807] return null if Seek Table is not supported. proguard in flac-extension --- extensions/flac/proguard-rules.txt | 10 ++++++++ .../exoplayer2/ext/flac/FlacDecoderJni.java | 4 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 4 +++- extensions/flac/src/main/jni/flac_jni.cc | 24 +++++++++++-------- extensions/flac/src/main/jni/flac_parser.cc | 3 +-- library/core/proguard-rules.txt | 10 -------- 6 files changed, 30 insertions(+), 25 deletions(-) diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index 3e52f643e7..b308b69a59 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -15,3 +15,13 @@ -keep class com.google.android.exoplayer2.metadata.flac.PictureFrame { *; } + +-dontnote com.google.android.exoplayer2.extractor.SeekPoint +-keepclasseswithmembers class com.google.android.exoplayer2.extractor.SeekPoint { + (long, long); +} + +-dontnote com.google.android.exoplayer2.extractor.SeekMap$SeekPoints +-keepclasseswithmembers class com.google.android.exoplayer2.extractor.SeekMap$SeekPoints { + (com.google.android.exoplayer2.extractor.SeekPoint, com.google.android.exoplayer2.extractor.SeekPoint); +} \ No newline at end of file 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 ce90f09d47..d4544e8700 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 @@ -221,10 +221,10 @@ import java.nio.ByteBuffer; * stream. * * @param timeUs A seek position in microseconds. - * @return The corresponding SeekPoints in the flac seek table or - * {@link com.google.android.exoplayer2.extractor.SeekPoint#position} set -1 if the stream doesn't + * @return The corresponding SeekPoints in the flac seek table or null if the stream doesn't * have a seek table. */ + @Nullable public SeekMap.SeekPoints getSeekPoints(long timeUs) { return flacGetSeekPoints(nativeDecoderContext, timeUs); } 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 08343ab38c..658c628448 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +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; @@ -341,7 +342,8 @@ public final class FlacExtractor implements Extractor { @Override public SeekPoints getSeekPoints(long timeUs) { - return decoderJni.getSeekPoints(timeUs); + @Nullable SeekPoints seekPoints = decoderJni.getSeekPoints(timeUs); + return seekPoints == null ? new SeekPoints(SeekPoint.START) : seekPoints; } @Override diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 85f41ec6c1..8c3ec419bf 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -203,16 +203,20 @@ DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) { DECODER_FUNC(jobject, flacGetSeekPoints, jlong jContext, jlong timeUs) { Context *context = reinterpret_cast(jContext); int64_t *result = context->parser->getSeekPositions(timeUs); - jclass seekPointClass = env->FindClass("com/google/android/exoplayer2/extractor/SeekPoint"); - jclass seekPointsClass = env->FindClass("com/google/android/exoplayer2/extractor/SeekMap$SeekPoints"); - jmethodID cidSeekPoint = env->GetMethodID(seekPointClass, "", "(JJ)V"); - jmethodID cidSeekPoints = env->GetMethodID(seekPointsClass, "", "(Lcom/google/android/exoplayer2/extractor/SeekPoint;Lcom/google/android/exoplayer2/extractor/SeekPoint;)V"); - jobject jSeekPointFirst = env->NewObject(seekPointClass, cidSeekPoint, (jlong) result[0], (jlong) result[1]); - jobject jSeekPointSecond = env->NewObject(seekPointClass, cidSeekPoint, (jlong) result[2], (jlong) result[3]); - jobject jSeekPoints = env->NewObject(seekPointsClass, cidSeekPoints, jSeekPointFirst, jSeekPointSecond); - env->DeleteLocalRef(jSeekPointFirst); - env->DeleteLocalRef(jSeekPointSecond); - return jSeekPoints; + if (result != NULL) { + jclass seekPointClass = env->FindClass("com/google/android/exoplayer2/extractor/SeekPoint"); + jclass seekPointsClass = env->FindClass("com/google/android/exoplayer2/extractor/SeekMap$SeekPoints"); + jmethodID cidSeekPoint = env->GetMethodID(seekPointClass, "", "(JJ)V"); + jmethodID cidSeekPoints = env->GetMethodID(seekPointsClass, "", "(Lcom/google/android/exoplayer2/extractor/SeekPoint;Lcom/google/android/exoplayer2/extractor/SeekPoint;)V"); + jobject jSeekPointFirst = env->NewObject(seekPointClass, cidSeekPoint, (jlong) result[0], (jlong) result[1]); + jobject jSeekPointSecond = env->NewObject(seekPointClass, cidSeekPoint, (jlong) result[2], (jlong) result[3]); + jobject jSeekPoints = env->NewObject(seekPointsClass, cidSeekPoints, jSeekPointFirst, jSeekPointSecond); + env->DeleteLocalRef(jSeekPointFirst); + env->DeleteLocalRef(jSeekPointSecond); + return jSeekPoints; + } else { + return NULL; + } } DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 1d4daca6a0..77ce030d91 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -440,9 +440,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { int64_t* FLACParser::getSeekPositions(int64_t timeUs) { int64_t *result = new int64_t[4]; - memset(result, -1, sizeof(result)); if (!mSeekTable) { - return result; + return NULL; } int64_t sample = (timeUs * getSampleRate()) / 1000000LL; diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index c3d71f0715..ab3cc5fccd 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -58,16 +58,6 @@ (com.google.android.exoplayer2.upstream.DataSource$Factory); } --dontnote com.google.android.exoplayer2.extractor.SeekPoint --keepclasseswithmembers class com.google.android.exoplayer2.extractor.SeekPoint { - (long, long); -} - --dontnote com.google.android.exoplayer2.extractor.SeekMap$SeekPoints --keepclasseswithmembers class com.google.android.exoplayer2.extractor.SeekMap$SeekPoints { - (com.google.android.exoplayer2.extractor.SeekPoint, com.google.android.exoplayer2.extractor.SeekPoint); -} - # Don't warn about checkerframework and Kotlin annotations -dontwarn org.checkerframework.** -dontwarn kotlin.annotations.jvm.** From 8ce8e351e15fb88b5261953e6603c10aee47beb8 Mon Sep 17 00:00:00 2001 From: wingyippp Date: Thu, 3 Oct 2019 14:40:39 +0800 Subject: [PATCH 498/807] new int64_t[4] after check mSeekTable supported --- extensions/flac/src/main/jni/flac_parser.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 77ce030d91..599be500be 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -439,10 +439,10 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { } int64_t* FLACParser::getSeekPositions(int64_t timeUs) { - int64_t *result = new int64_t[4]; if (!mSeekTable) { return NULL; } + int64_t *result = new int64_t[4]; int64_t sample = (timeUs * getSampleRate()) / 1000000LL; if (sample >= getTotalSamples()) { From 9ec94a4bdc0678ec4a894d7adde7379b494a65af Mon Sep 17 00:00:00 2001 From: Cai Yuanqing Date: Fri, 4 Oct 2019 13:55:25 +1300 Subject: [PATCH 499/807] Check the new index contains the old index based on stat time instead of segNum --- .../exoplayer2/source/dash/DefaultDashChunkSource.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 14fe81f605..6218fb01d0 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 @@ -686,6 +686,8 @@ public class DefaultDashChunkSource implements DashChunkSource { newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex); } + long oldIndexFirstSegmentNum = oldIndex.getFirstSegmentNum(); + long oldIndexStartTimeUs = oldIndex.getTimeUs(oldIndexFirstSegmentNum); long oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) @@ -700,8 +702,10 @@ public class DefaultDashChunkSource implements DashChunkSource { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. throw new BehindLiveWindowException(); - } else if (oldIndex.getFirstSegmentNum() >= newIndexFirstSegmentNum) { - // The new index contains the old one, continue process the next segment + } else if (oldIndexStartTimeUs >= newIndexStartTimeUs) { + // The new index overlaps with (but does not have a start position contained within) the old + // index. This can only happen if extra segments have been added to the start of the index. + // Continue process the next segment as is. } else { // The new index overlaps with the old one. newSegmentNumShift += From fbb4715646e9de4468f46a0872ce1cb0c1c4aa8a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Oct 2019 09:55:27 +0100 Subject: [PATCH 500/807] Minor upstream cleanup PiperOrigin-RevId: 272614610 --- .../android/exoplayer2/upstream/cache/CacheDataSink.java | 3 +-- .../exoplayer2/upstream/crypto/AesCipherDataSink.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) 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 80fecf19cc..22ed3892ec 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 @@ -53,7 +53,6 @@ public final class CacheDataSink implements DataSink { private long dataSpecFragmentSize; private File file; private OutputStream outputStream; - private FileOutputStream underlyingFileOutputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; private ReusableBufferedOutputStream bufferedOutputStream; @@ -171,7 +170,7 @@ public final class CacheDataSink implements DataSink { file = cache.startFile( dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length); - underlyingFileOutputStream = new FileOutputStream(file); + FileOutputStream underlyingFileOutputStream = new FileOutputStream(file); if (bufferSize > 0) { if (bufferedOutputStream == null) { bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream, 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 522fdc9a3f..d9b3ff0069 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 @@ -52,10 +52,10 @@ public final class AesCipherDataSink implements DataSink { * * @param secretKey The key data. * @param wrappedDataSink The wrapped {@link DataSink}. - * @param scratch Scratch space. Data is decrypted into this array before being written to the + * @param scratch Scratch space. Data is encrypted 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. If {@code null} then decryption + * cipher calls will be required to complete the operation. If {@code null} then encryption * will overwrite the input {@code data}. */ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) { @@ -96,5 +96,4 @@ public final class AesCipherDataSink implements DataSink { cipher = null; wrappedDataSink.close(); } - } From e377e13d50f7d8b5909d0fca7f6840d1dae6fe1b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Oct 2019 09:56:52 +0100 Subject: [PATCH 501/807] Clean up DashManifestParserTest Also improve some tests by asserting the parser is left in the correct position (assertNextTag). PiperOrigin-RevId: 272614768 --- .../test/assets/{sample_mpd_1 => sample_mpd} | 1 - ...4_event_stream => sample_mpd_event_stream} | 1 - ...t_template => sample_mpd_segment_template} | 1 - ...mime_type => sample_mpd_unknown_mime_type} | 1 - .../dash/manifest/DashManifestParserTest.java | 182 ++++++++++-------- 5 files changed, 99 insertions(+), 87 deletions(-) rename library/dash/src/test/assets/{sample_mpd_1 => sample_mpd} (99%) rename library/dash/src/test/assets/{sample_mpd_4_event_stream => sample_mpd_event_stream} (99%) rename library/dash/src/test/assets/{sample_mpd_3_segment_template => sample_mpd_segment_template} (99%) rename library/dash/src/test/assets/{sample_mpd_2_unknown_mime_type => sample_mpd_unknown_mime_type} (99%) diff --git a/library/dash/src/test/assets/sample_mpd_1 b/library/dash/src/test/assets/sample_mpd similarity index 99% rename from library/dash/src/test/assets/sample_mpd_1 rename to library/dash/src/test/assets/sample_mpd index ccd3ab4dd6..8417d2f7c4 100644 --- a/library/dash/src/test/assets/sample_mpd_1 +++ b/library/dash/src/test/assets/sample_mpd @@ -102,4 +102,3 @@ http://www.test.com/vtt - diff --git a/library/dash/src/test/assets/sample_mpd_4_event_stream b/library/dash/src/test/assets/sample_mpd_event_stream similarity index 99% rename from library/dash/src/test/assets/sample_mpd_4_event_stream rename to library/dash/src/test/assets/sample_mpd_event_stream index e4c927260d..4148b420f1 100644 --- a/library/dash/src/test/assets/sample_mpd_4_event_stream +++ b/library/dash/src/test/assets/sample_mpd_event_stream @@ -44,4 +44,3 @@ - diff --git a/library/dash/src/test/assets/sample_mpd_3_segment_template b/library/dash/src/test/assets/sample_mpd_segment_template similarity index 99% rename from library/dash/src/test/assets/sample_mpd_3_segment_template rename to library/dash/src/test/assets/sample_mpd_segment_template index a9147b54df..d45ab14f52 100644 --- a/library/dash/src/test/assets/sample_mpd_3_segment_template +++ b/library/dash/src/test/assets/sample_mpd_segment_template @@ -35,4 +35,3 @@ - diff --git a/library/dash/src/test/assets/sample_mpd_2_unknown_mime_type b/library/dash/src/test/assets/sample_mpd_unknown_mime_type similarity index 99% rename from library/dash/src/test/assets/sample_mpd_2_unknown_mime_type rename to library/dash/src/test/assets/sample_mpd_unknown_mime_type index c6f00965e3..4645e3c859 100644 --- a/library/dash/src/test/assets/sample_mpd_2_unknown_mime_type +++ b/library/dash/src/test/assets/sample_mpd_unknown_mime_type @@ -115,4 +115,3 @@ http://www.test.com/vtt - diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 6f5bc5c83d..d2a4f1cd6f 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -40,32 +40,35 @@ import org.xmlpull.v1.XmlPullParserFactory; @RunWith(AndroidJUnit4.class) public class DashManifestParserTest { - private static final String SAMPLE_MPD_1 = "sample_mpd_1"; - private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type"; - private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template"; - private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream"; + private static final String SAMPLE_MPD = "sample_mpd"; + private static final String SAMPLE_MPD_UNKNOWN_MIME_TYPE = "sample_mpd_unknown_mime_type"; + private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = "sample_mpd_segment_template"; + private static final String SAMPLE_MPD_EVENT_STREAM = "sample_mpd_event_stream"; + + private static final String NEXT_TAG_NAME = "Next"; + private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */ @Test - public void testParseMediaPresentationDescription() throws IOException { + public void parseMediaPresentationDescription() throws IOException { DashManifestParser parser = new DashManifestParser(); parser.parse( Uri.parse("https://example.com/test.mpd"), - TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_1)); + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD)); parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_UNKNOWN_MIME_TYPE)); } @Test - public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException { + public void parseMediaPresentationDescription_segmentTemplate() throws IOException { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), SAMPLE_MPD_3_SEGMENT_TEMPLATE)); + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_SEGMENT_TEMPLATE)); assertThat(mpd.getPeriodCount()).isEqualTo(1); @@ -91,13 +94,13 @@ public class DashManifestParserTest { } @Test - public void testParseMediaPresentationDescriptionCanParseEventStream() throws IOException { + public void parseMediaPresentationDescription_eventStream() throws IOException { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), SAMPLE_MPD_4_EVENT_STREAM)); + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_EVENT_STREAM)); Period period = mpd.getPeriod(0); assertThat(period.eventStreams).hasSize(3); @@ -161,12 +164,12 @@ public class DashManifestParserTest { } @Test - public void testParseMediaPresentationDescriptionCanParseProgramInformation() throws IOException { + public void parseMediaPresentationDescription_programInformation() throws IOException { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( Uri.parse("Https://example.com/test.mpd"), - TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_1)); + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD)); ProgramInformation expectedProgramInformation = new ProgramInformation( "MediaTitle", "MediaSource", "MediaCopyright", "www.example.com", "enUs"); @@ -174,7 +177,82 @@ public class DashManifestParserTest { } @Test - public void testParseCea608AccessibilityChannel() { + public void parseSegmentTimeline_repeatCount() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput( + new StringReader( + "" + + NEXT_TAG)); + xpp.next(); + + List elements = + parser.parseSegmentTimeline(xpp, /* timescale= */ 48000, /* periodDurationMs= */ 10000); + + assertThat(elements) + .containsExactly( + new SegmentTimelineElement(/* startTime= */ 0, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 96000, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 192000, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 288000, /* duration= */ 48000)) + .inOrder(); + assertNextTag(xpp); + } + + @Test + public void parseSegmentTimeline_singleUndefinedRepeatCount() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput( + new StringReader( + "" + NEXT_TAG)); + xpp.next(); + + List elements = + parser.parseSegmentTimeline(xpp, /* timescale= */ 48000, /* periodDurationMs= */ 10000); + + assertThat(elements) + .containsExactly( + new SegmentTimelineElement(/* startTime= */ 0, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 96000, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 192000, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 288000, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 384000, /* duration= */ 96000)) + .inOrder(); + assertNextTag(xpp); + } + + @Test + public void parseSegmentTimeline_timeOffsetsAndUndefinedRepeatCount() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput( + new StringReader( + "" + + "" + + NEXT_TAG)); + xpp.next(); + + List elements = + parser.parseSegmentTimeline(xpp, /* timescale= */ 48000, /* periodDurationMs= */ 10000); + + assertThat(elements) + .containsExactly( + new SegmentTimelineElement(/* startTime= */ 0, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 96000, /* duration= */ 96000), + new SegmentTimelineElement(/* startTime= */ 192000, /* duration= */ 48000), + new SegmentTimelineElement(/* startTime= */ 240000, /* duration= */ 48000), + new SegmentTimelineElement(/* startTime= */ 288000, /* duration= */ 48000), + new SegmentTimelineElement(/* startTime= */ 336000, /* duration= */ 48000), + new SegmentTimelineElement(/* startTime= */ 384000, /* duration= */ 48000), + new SegmentTimelineElement(/* startTime= */ 432000, /* duration= */ 48000)) + .inOrder(); + assertNextTag(xpp); + } + + @Test + public void parseCea608AccessibilityChannel() { assertThat( DashManifestParser.parseCea608AccessibilityChannel( buildCea608AccessibilityDescriptors("CC1=eng"))) @@ -215,7 +293,7 @@ public class DashManifestParserTest { } @Test - public void testParseCea708AccessibilityChannel() { + public void parseCea708AccessibilityChannel() { assertThat( DashManifestParser.parseCea708AccessibilityChannel( buildCea708AccessibilityDescriptors("1=lang:eng"))) @@ -259,74 +337,6 @@ public class DashManifestParserTest { .isEqualTo(Format.NO_VALUE); } - @Test - public void parseSegmentTimeline_withRepeatCount() throws Exception { - DashManifestParser parser = new DashManifestParser(); - XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); - xpp.setInput( - new StringReader( - "")); - xpp.next(); - - List elements = - parser.parseSegmentTimeline(xpp, /* timescale= */ 48000, /* periodDurationMs= */ 10000); - - assertThat(elements) - .containsExactly( - new SegmentTimelineElement(/* startTime= */ 0, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 96000, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 192000, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 288000, /* duration= */ 48000)) - .inOrder(); - } - - @Test - public void parseSegmentTimeline_withSingleUndefinedRepeatCount() throws Exception { - DashManifestParser parser = new DashManifestParser(); - XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); - xpp.setInput(new StringReader("")); - xpp.next(); - - List elements = - parser.parseSegmentTimeline(xpp, /* timescale= */ 48000, /* periodDurationMs= */ 10000); - - assertThat(elements) - .containsExactly( - new SegmentTimelineElement(/* startTime= */ 0, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 96000, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 192000, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 288000, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 384000, /* duration= */ 96000)) - .inOrder(); - } - - @Test - public void parseSegmentTimeline_withTimeOffsetsAndUndefinedRepeatCount() throws Exception { - DashManifestParser parser = new DashManifestParser(); - XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); - xpp.setInput( - new StringReader( - "" - + "")); - xpp.next(); - - List elements = - parser.parseSegmentTimeline(xpp, /* timescale= */ 48000, /* periodDurationMs= */ 10000); - - assertThat(elements) - .containsExactly( - new SegmentTimelineElement(/* startTime= */ 0, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 96000, /* duration= */ 96000), - new SegmentTimelineElement(/* startTime= */ 192000, /* duration= */ 48000), - new SegmentTimelineElement(/* startTime= */ 240000, /* duration= */ 48000), - new SegmentTimelineElement(/* startTime= */ 288000, /* duration= */ 48000), - new SegmentTimelineElement(/* startTime= */ 336000, /* duration= */ 48000), - new SegmentTimelineElement(/* startTime= */ 384000, /* duration= */ 48000), - new SegmentTimelineElement(/* startTime= */ 432000, /* duration= */ 48000)) - .inOrder(); - } - private static List buildCea608AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } @@ -334,4 +344,10 @@ public class DashManifestParserTest { private static List buildCea708AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null)); } + + private static void assertNextTag(XmlPullParser xpp) throws Exception { + xpp.next(); + assertThat(xpp.getEventType()).isEqualTo(XmlPullParser.START_TAG); + assertThat(xpp.getName()).isEqualTo(NEXT_TAG_NAME); + } } From 3c235dfc1ff6546d19c686de3eee3a2b58092c23 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Oct 2019 09:57:55 +0100 Subject: [PATCH 502/807] Make factories return specific types PiperOrigin-RevId: 272614917 --- .../android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java | 2 +- .../android/exoplayer2/upstream/FileDataSourceFactory.java | 2 +- .../android/exoplayer2/upstream/ResolvingDataSource.java | 6 ++---- .../google/android/exoplayer2/testutil/FakeDataSource.java | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) 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 505724e846..db60eea269 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 @@ -37,7 +37,7 @@ public final class RtmpDataSourceFactory implements DataSource.Factory { } @Override - public DataSource createDataSource() { + public RtmpDataSource createDataSource() { RtmpDataSource dataSource = new RtmpDataSource(); if (listener != null) { dataSource.addTransferListener(listener); 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 0b4de1b43e..e0630c7989 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 @@ -33,7 +33,7 @@ public final class FileDataSourceFactory implements DataSource.Factory { } @Override - public DataSource createDataSource() { + public FileDataSource createDataSource() { FileDataSource dataSource = new FileDataSource(); if (listener != null) { dataSource.addTransferListener(listener); 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 index 99f0dee207..412f866e99 100644 --- 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 @@ -64,9 +64,7 @@ public final class ResolvingDataSource implements DataSource { private final Resolver resolver; /** - * Creates factory for {@link ResolvingDataSource} instances. - * - * @param upstreamFactory The wrapped {@link DataSource.Factory} handling the resolved {@link + * @param upstreamFactory The wrapped {@link DataSource.Factory} for handling resolved {@link * DataSpec DataSpecs}. * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. */ @@ -76,7 +74,7 @@ public final class ResolvingDataSource implements DataSource { } @Override - public DataSource createDataSource() { + public ResolvingDataSource createDataSource() { return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 9f6fdc9d49..ab7c5be5b2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -52,7 +52,7 @@ public class FakeDataSource extends BaseDataSource { } @Override - public DataSource createDataSource() { + public FakeDataSource createDataSource() { return new FakeDataSource(fakeDataSet, isNetwork); } } From 737b5cd82dd4a8f6705a38a95ae0bfc76c863fcf Mon Sep 17 00:00:00 2001 From: sofijajvc Date: Thu, 3 Oct 2019 10:23:05 +0100 Subject: [PATCH 503/807] Fix GL error logging Log only if an error occured. PiperOrigin-RevId: 272618322 --- .../main/java/com/google/android/exoplayer2/util/GlUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 7fc46dc363..c7feff516a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -42,7 +42,7 @@ public final class GlUtil { int lastError = GLES20.GL_NO_ERROR; int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { - Log.e(TAG, "glError " + gluErrorString(lastError)); + Log.e(TAG, "glError " + gluErrorString(error)); lastError = error; } if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) { From 433526e0343c109cab18a3433e8d7505962a6ee3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 3 Oct 2019 13:54:18 +0100 Subject: [PATCH 504/807] Deprecate DefaultDrmSessionManager factory methods and migrate main demo app PiperOrigin-RevId: 272643202 --- .../exoplayer2/demo/PlayerActivity.java | 85 ++++++------------- demos/main/src/main/res/values/strings.xml | 2 +- .../drm/DefaultDrmSessionManager.java | 8 +- 3 files changed, 36 insertions(+), 59 deletions(-) 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 347f49e27c..1dc56bfbc9 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.demo; import android.content.Intent; import android.content.pm.PackageManager; +import android.media.MediaDrm; import android.net.Uri; import android.os.Bundle; import android.util.Pair; @@ -40,10 +41,10 @@ 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.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -78,8 +79,6 @@ 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 @@ -131,8 +130,6 @@ 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; @@ -156,10 +153,6 @@ public class PlayerActivity extends AppCompatActivity private AdsLoader adsLoader; private Uri loadedAdTagUri; - public PlayerActivity() { - mediaDrms = new ArrayList<>(); - } - // Activity lifecycle @Override @@ -342,7 +335,6 @@ public class PlayerActivity extends AppCompatActivity if (player == null) { Intent intent = getIntent(); - releaseMediaDrms(); mediaSource = createTopLevelMediaSource(intent); if (mediaSource == null) { return; @@ -452,38 +444,29 @@ public class PlayerActivity extends AppCompatActivity } 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 { + int errorStringId = R.string.error_drm_unknown; + DrmSessionManager drmSessionManager = null; + if (drmInfo == null) { drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } else if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_unsupported_before_api_18; + } else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) { + errorStringId = R.string.error_drm_unsupported_scheme; + } else { + MediaDrmCallback mediaDrmCallback = + createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER) + .setMultiSession(drmInfo.drmMultiSession) + .build(mediaDrmCallback); + } + + if (drmSessionManager == null) { + showToast(errorStringId); + finish(); + return null; } DownloadRequest downloadRequest = @@ -497,7 +480,7 @@ public class PlayerActivity extends AppCompatActivity } private MediaSource createLeafMediaSource( - Uri uri, String extension, DrmSessionManager drmSessionManager) { + Uri uri, String extension, DrmSessionManager drmSessionManager) { @ContentType int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: @@ -521,9 +504,8 @@ public class PlayerActivity extends AppCompatActivity } } - private DefaultDrmSessionManager buildDrmSessionManagerV18( - UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) - throws UnsupportedDrmException { + private HttpMediaDrmCallback createMediaDrmCallback( + String licenseUrl, String[] keyRequestPropertiesArray) { HttpDataSource.Factory licenseDataSourceFactory = ((DemoApplication) getApplication()).buildHttpDataSourceFactory(); HttpMediaDrmCallback drmCallback = @@ -534,10 +516,7 @@ public class PlayerActivity extends AppCompatActivity keyRequestPropertiesArray[i + 1]); } } - - FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); - mediaDrms.add(mediaDrm); - return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); + return drmCallback; } private void releasePlayer() { @@ -554,14 +533,6 @@ public class PlayerActivity extends AppCompatActivity if (adsLoader != null) { adsLoader.setPlayer(null); } - releaseMediaDrms(); - } - - private void releaseMediaDrms() { - for (FrameworkMediaDrm mediaDrm : mediaDrms) { - mediaDrm.release(); - } - mediaDrms.clear(); } private void releaseAdsLoader() { diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index f74ce8c076..c39fffa65d 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -29,7 +29,7 @@ Unrecognized stereo mode - Protected content not supported on API levels below 18 + Protected content not supported on API levels below 18 This device does not support the required DRM scheme 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 e8a6fe6572..731b984ab8 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 @@ -63,7 +63,7 @@ public class DefaultDrmSessionManager private LoadErrorHandlingPolicy loadErrorHandlingPolicy; /** - * Creates a builder with default values. + * Creates a builder with default values. The default values are: * *

        *
      • {@code surface_type} - The type of surface view used for video playbacks. Valid - * values are {@code surface_view}, {@code texture_view}, {@code spherical_view} and {@code - * none}. Using {@code none} is recommended for audio only applications, since creating the - * surface can be expensive. Using {@code surface_view} is recommended for video applications. - * Note, TextureView can only be used in a hardware accelerated window. When rendered in - * software, TextureView will draw nothing. + * values are {@code surface_view}, {@code texture_view}, {@code spherical_gl_surface_view}, + * {@code video_decoder_gl_surface_view} and {@code none}. Using {@code none} is recommended + * for audio only applications, since creating the surface can be expensive. Using {@code + * surface_view} is recommended for video applications. Note, TextureView can only be used in + * a hardware accelerated window. When rendered in software, TextureView will draw nothing. *
          *
        • Corresponding method: None *
        • Default: {@code surface_view} @@ -276,8 +276,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private static final int SURFACE_TYPE_NONE = 0; private static final int SURFACE_TYPE_SURFACE_VIEW = 1; private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; - private static final int SURFACE_TYPE_MONO360_VIEW = 3; - private static final int SURFACE_TYPE_VIDEO_GL_SURFACE_VIEW = 4; + private static final int SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3; + private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4; // LINT.ThenChange(../../../../../../res/values/attrs.xml) @Nullable private final AspectRatioFrameLayout contentFrame; @@ -409,13 +409,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider case SURFACE_TYPE_TEXTURE_VIEW: surfaceView = new TextureView(context); break; - case SURFACE_TYPE_MONO360_VIEW: - SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); - sphericalSurfaceView.setSingleTapListener(componentListener); - surfaceView = sphericalSurfaceView; + case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW: + SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context); + sphericalGLSurfaceView.setSingleTapListener(componentListener); + surfaceView = sphericalGLSurfaceView; break; - case SURFACE_TYPE_VIDEO_GL_SURFACE_VIEW: - surfaceView = new VideoDecoderSurfaceView(context); + case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW: + surfaceView = new VideoDecoderGLSurfaceView(context); break; default: surfaceView = new SurfaceView(context); @@ -547,10 +547,10 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider oldVideoComponent.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SphericalSurfaceView) { - ((SphericalSurfaceView) surfaceView).setVideoComponent(null); - } else if (surfaceView instanceof VideoDecoderSurfaceView) { - oldVideoComponent.setOutputBufferRenderer(null); + } else if (surfaceView instanceof SphericalGLSurfaceView) { + ((SphericalGLSurfaceView) surfaceView).setVideoComponent(null); + } else if (surfaceView instanceof VideoDecoderGLSurfaceView) { + oldVideoComponent.setVideoDecoderOutputBufferRenderer(null); } else if (surfaceView instanceof SurfaceView) { oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); } @@ -575,11 +575,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (newVideoComponent != null) { if (surfaceView instanceof TextureView) { newVideoComponent.setVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SphericalSurfaceView) { - ((SphericalSurfaceView) surfaceView).setVideoComponent(newVideoComponent); - } else if (surfaceView instanceof VideoDecoderSurfaceView) { - newVideoComponent.setOutputBufferRenderer( - ((VideoDecoderSurfaceView) surfaceView).getOutputBufferRenderer()); + } else if (surfaceView instanceof SphericalGLSurfaceView) { + ((SphericalGLSurfaceView) surfaceView).setVideoComponent(newVideoComponent); + } else if (surfaceView instanceof VideoDecoderGLSurfaceView) { + newVideoComponent.setVideoDecoderOutputBufferRenderer( + ((VideoDecoderGLSurfaceView) surfaceView).getVideoDecoderOutputBufferRenderer()); } else if (surfaceView instanceof SurfaceView) { newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); } @@ -1049,12 +1049,15 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider *
        • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code * surface_view}. *
        • {@link TextureView} if {@code surface_type} is {@code texture_view}. - *
        • {@link SphericalSurfaceView} if {@code surface_type} is {@code spherical_view}. + *
        • {@link SphericalGLSurfaceView} if {@code surface_type} is {@code + * spherical_gl_surface_view}. + *
        • {@link VideoDecoderGLSurfaceView} if {@code surface_type} is {@code + * video_decoder_gl_surface_view}. *
        • {@code null} if {@code surface_type} is {@code none}. *
        * - * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code - * null}. + * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalGLSurfaceView}, {@link + * VideoDecoderGLSurfaceView} or {@code null}. */ @Nullable public View getVideoSurfaceView() { @@ -1122,34 +1125,34 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** * Should be called when the player is visible to the user and if {@code surface_type} is {@code - * spherical_view}. It is the counterpart to {@link #onPause()}. + * spherical_gl_surface_view}. It is the counterpart to {@link #onPause()}. * *

        This method should typically be called in {@code Activity.onStart()}, or {@code * Activity.onResume()} for API versions <= 23. */ public void onResume() { - if (surfaceView instanceof SphericalSurfaceView) { - ((SphericalSurfaceView) surfaceView).onResume(); + if (surfaceView instanceof SphericalGLSurfaceView) { + ((SphericalGLSurfaceView) surfaceView).onResume(); } } /** * Should be called when the player is no longer visible to the user and if {@code surface_type} - * is {@code spherical_view}. It is the counterpart to {@link #onResume()}. + * is {@code spherical_gl_surface_view}. It is the counterpart to {@link #onResume()}. * *

        This method should typically be called in {@code Activity.onStop()}, or {@code * Activity.onPause()} for API versions <= 23. */ public void onPause() { - if (surfaceView instanceof SphericalSurfaceView) { - ((SphericalSurfaceView) surfaceView).onPause(); + if (surfaceView instanceof SphericalGLSurfaceView) { + ((SphericalGLSurfaceView) surfaceView).onPause(); } } /** * Called when there's a change in the aspect ratio of the content being displayed. The default * implementation sets the aspect ratio of the content frame to that of the content, unless the - * content view is a {@link SphericalSurfaceView} in which case the frame's aspect ratio is + * content view is a {@link SphericalGLSurfaceView} in which case the frame's aspect ratio is * cleared. * * @param contentAspectRatio The aspect ratio of the content. @@ -1162,7 +1165,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Nullable View contentView) { if (contentFrame != null) { contentFrame.setAspectRatio( - contentView instanceof SphericalSurfaceView ? 0 : contentAspectRatio); + contentView instanceof SphericalGLSurfaceView ? 0 : contentAspectRatio); } } 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/SphericalGLSurfaceView.java similarity index 98% rename from library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java index d2089759f6..c01fccf54b 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/SphericalGLSurfaceView.java @@ -51,7 +51,7 @@ import javax.microedition.khronos.opengles.GL10; * apply the touch and sensor rotations in the correct order or the user's touch manipulations won't * match what they expect. */ -public final class SphericalSurfaceView extends GLSurfaceView { +public final class SphericalGLSurfaceView extends GLSurfaceView { // Arbitrary vertical field of view. private static final int FIELD_OF_VIEW_DEGREES = 90; @@ -73,11 +73,11 @@ public final class SphericalSurfaceView extends GLSurfaceView { @Nullable private Surface surface; @Nullable private Player.VideoComponent videoComponent; - public SphericalSurfaceView(Context context) { + public SphericalGLSurfaceView(Context context) { this(context, null); } - public SphericalSurfaceView(Context context, @Nullable AttributeSet attributeSet) { + public SphericalGLSurfaceView(Context context, @Nullable AttributeSet attributeSet) { super(context, attributeSet); mainHandler = new Handler(Looper.getMainLooper()); 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 66a0f20091..20b7dc0319 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 @@ -75,7 +75,7 @@ import androidx.annotation.Nullable; this.listener = listener; this.pxPerDegrees = pxPerDegrees; gestureDetector = new GestureDetector(context, this); - roll = SphericalSurfaceView.UPRIGHT_ROLL; + roll = SphericalGLSurfaceView.UPRIGHT_ROLL; } public void setSingleTapListener(@Nullable SingleTapListener listener) { diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index b342d5d888..535bf320fb 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -29,8 +29,8 @@ - - + + From ab2bfcc1b9c96c77fde4ab390d8ada1eb7aa58b1 Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 1 Nov 2019 12:29:29 +0000 Subject: [PATCH 663/807] Fix typo in WavHeader class PiperOrigin-RevId: 277910360 --- .../com/google/android/exoplayer2/extractor/wav/WavHeader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6e3c5988a9..228151339a 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 @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ /* package */ final class WavHeader implements SeekMap { - /** Number of audio chanels. */ + /** Number of audio channels. */ private final int numChannels; /** Sample rate in Hertz. */ private final int sampleRateHz; From b972fd1f27d8ca657796423c0c9bc7cda07dd5aa Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 12:36:23 +0000 Subject: [PATCH 664/807] Remove WebvttCue from null-checking blacklist PiperOrigin-RevId: 277910909 --- .../google/android/exoplayer2/text/webvtt/WebvttCue.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index 0447e8c477..eae879c21b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -21,6 +21,7 @@ import android.text.Layout.Alignment; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -39,7 +40,7 @@ public final class WebvttCue extends Cue { long startTime, long endTime, CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, @@ -129,6 +130,9 @@ public final class WebvttCue extends Cue { // Initialization methods + // Calling reset() is forbidden because `this` isn't initialized. This can be safely + // suppressed because reset() only assigns fields, it doesn't read any. + @SuppressWarnings("nullness:method.invocation.invalid") public Builder() { reset(); } @@ -168,7 +172,7 @@ public final class WebvttCue extends Cue { return new WebvttCue( startTime, endTime, - text, + Assertions.checkNotNull(text), convertTextAlignment(textAlignment), line, lineType, @@ -277,6 +281,7 @@ public final class WebvttCue extends Cue { } } + @Nullable private static Alignment convertTextAlignment(@TextAlignment int textAlignment) { switch (textAlignment) { case TextAlignment.START: From 5b80b4b523d9a79e1cc59d6af9d80e612fd4d131 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 1 Nov 2019 12:38:50 +0000 Subject: [PATCH 665/807] Update initial bitrate estimates. PiperOrigin-RevId: 277911191 --- .../upstream/DefaultBandwidthMeter.java | 345 +++++++++--------- 1 file changed, 171 insertions(+), 174 deletions(-) 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 da120414b4..f688bb9447 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 @@ -56,19 +56,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default initial Wifi bitrate estimate in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = - new long[] {5_400_000, 3_400_000, 1_900_000, 1_100_000, 400_000}; + new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000}; /** Default initial 2G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = - new long[] {170_000, 139_000, 122_000, 107_000, 90_000}; + new long[] {200_000, 148_000, 132_000, 115_000, 95_000}; /** Default initial 3G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = - new long[] {2_100_000, 1_300_000, 960_000, 770_000, 450_000}; + new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000}; /** Default initial 4G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = - new long[] {6_000_000, 3_400_000, 2_100_000, 1_400_000, 570_000}; + new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000}; /** * Default initial bitrate estimate used when the device is offline or the network type cannot be @@ -487,247 +487,244 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private static Map createInitialBitrateCountryGroupAssignment() { HashMap countryGroupAssignment = new HashMap<>(); - countryGroupAssignment.put("AD", new int[] {1, 0, 0, 1}); + countryGroupAssignment.put("AD", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("AE", new int[] {1, 4, 4, 4}); countryGroupAssignment.put("AF", new int[] {4, 4, 3, 3}); - countryGroupAssignment.put("AG", new int[] {3, 2, 1, 1}); - countryGroupAssignment.put("AI", new int[] {1, 0, 1, 3}); - countryGroupAssignment.put("AL", new int[] {1, 2, 1, 1}); - countryGroupAssignment.put("AM", new int[] {2, 2, 3, 2}); + countryGroupAssignment.put("AG", new int[] {3, 1, 0, 1}); + countryGroupAssignment.put("AI", new int[] {1, 0, 0, 3}); + countryGroupAssignment.put("AL", new int[] {1, 2, 0, 1}); + countryGroupAssignment.put("AM", new int[] {2, 2, 2, 2}); countryGroupAssignment.put("AO", new int[] {3, 4, 2, 0}); - countryGroupAssignment.put("AQ", new int[] {4, 2, 2, 2}); countryGroupAssignment.put("AR", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("AS", new int[] {3, 3, 4, 1}); - countryGroupAssignment.put("AT", new int[] {0, 2, 0, 0}); - countryGroupAssignment.put("AU", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("AW", new int[] {1, 1, 0, 2}); - countryGroupAssignment.put("AX", new int[] {0, 2, 1, 0}); - countryGroupAssignment.put("AZ", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("BA", new int[] {1, 1, 1, 2}); - countryGroupAssignment.put("BB", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("BD", new int[] {2, 2, 3, 2}); + countryGroupAssignment.put("AS", new int[] {3, 0, 4, 2}); + countryGroupAssignment.put("AT", new int[] {0, 3, 0, 0}); + countryGroupAssignment.put("AU", new int[] {0, 3, 0, 1}); + countryGroupAssignment.put("AW", new int[] {1, 1, 0, 3}); + countryGroupAssignment.put("AX", new int[] {0, 3, 0, 2}); + countryGroupAssignment.put("AZ", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("BA", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("BB", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("BD", new int[] {2, 1, 3, 3}); countryGroupAssignment.put("BE", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("BF", new int[] {4, 4, 3, 1}); + countryGroupAssignment.put("BF", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("BG", new int[] {0, 1, 0, 0}); countryGroupAssignment.put("BH", new int[] {2, 1, 3, 4}); - countryGroupAssignment.put("BI", new int[] {4, 3, 4, 4}); - countryGroupAssignment.put("BJ", new int[] {4, 3, 4, 4}); - countryGroupAssignment.put("BL", new int[] {1, 0, 2, 3}); - countryGroupAssignment.put("BM", new int[] {1, 0, 0, 0}); - countryGroupAssignment.put("BN", new int[] {4, 2, 3, 3}); - countryGroupAssignment.put("BO", new int[] {2, 2, 3, 2}); - countryGroupAssignment.put("BQ", new int[] {1, 0, 3, 4}); + countryGroupAssignment.put("BI", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("BJ", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("BL", new int[] {1, 0, 2, 2}); + countryGroupAssignment.put("BM", new int[] {1, 2, 0, 0}); + countryGroupAssignment.put("BN", new int[] {4, 1, 3, 2}); + countryGroupAssignment.put("BO", new int[] {1, 2, 3, 2}); + countryGroupAssignment.put("BQ", new int[] {1, 1, 2, 4}); countryGroupAssignment.put("BR", new int[] {2, 3, 3, 2}); - countryGroupAssignment.put("BS", new int[] {2, 0, 1, 4}); - countryGroupAssignment.put("BT", new int[] {3, 0, 2, 1}); + countryGroupAssignment.put("BS", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("BT", new int[] {3, 0, 3, 1}); countryGroupAssignment.put("BW", new int[] {4, 4, 1, 2}); countryGroupAssignment.put("BY", new int[] {0, 1, 1, 2}); - countryGroupAssignment.put("BZ", new int[] {2, 2, 3, 1}); - countryGroupAssignment.put("CA", new int[] {0, 3, 3, 3}); - countryGroupAssignment.put("CD", new int[] {4, 4, 3, 2}); - countryGroupAssignment.put("CF", new int[] {4, 3, 3, 4}); - countryGroupAssignment.put("CG", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("CH", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("BZ", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("CA", new int[] {0, 3, 1, 3}); + countryGroupAssignment.put("CD", new int[] {4, 4, 2, 2}); + countryGroupAssignment.put("CF", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("CG", new int[] {3, 4, 2, 4}); + countryGroupAssignment.put("CH", new int[] {0, 0, 1, 0}); countryGroupAssignment.put("CI", new int[] {3, 4, 3, 3}); countryGroupAssignment.put("CK", new int[] {2, 4, 1, 0}); - countryGroupAssignment.put("CL", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("CM", new int[] {3, 4, 2, 1}); - countryGroupAssignment.put("CN", new int[] {2, 2, 2, 3}); + countryGroupAssignment.put("CL", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("CM", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("CN", new int[] {2, 0, 2, 3}); countryGroupAssignment.put("CO", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("CR", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("CR", new int[] {2, 3, 4, 4}); countryGroupAssignment.put("CU", new int[] {4, 4, 3, 1}); - countryGroupAssignment.put("CV", new int[] {2, 3, 2, 4}); - countryGroupAssignment.put("CW", new int[] {1, 0, 0, 0}); - countryGroupAssignment.put("CX", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("CY", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("CV", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("CW", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("CY", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("CZ", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("DE", new int[] {0, 2, 2, 2}); - countryGroupAssignment.put("DJ", new int[] {3, 3, 4, 0}); - countryGroupAssignment.put("DK", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("DM", new int[] {1, 0, 0, 3}); + countryGroupAssignment.put("DE", new int[] {0, 1, 1, 3}); + countryGroupAssignment.put("DJ", new int[] {4, 3, 4, 1}); + countryGroupAssignment.put("DK", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("DM", new int[] {1, 0, 1, 3}); countryGroupAssignment.put("DO", new int[] {3, 3, 4, 4}); countryGroupAssignment.put("DZ", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("EC", new int[] {2, 4, 4, 2}); - countryGroupAssignment.put("EE", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("EC", new int[] {2, 3, 4, 3}); + countryGroupAssignment.put("EE", new int[] {0, 1, 0, 0}); countryGroupAssignment.put("EG", new int[] {3, 4, 2, 2}); countryGroupAssignment.put("EH", new int[] {2, 0, 3, 3}); - countryGroupAssignment.put("ER", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("ER", new int[] {4, 2, 2, 0}); countryGroupAssignment.put("ES", new int[] {0, 1, 1, 1}); countryGroupAssignment.put("ET", new int[] {4, 4, 4, 0}); countryGroupAssignment.put("FI", new int[] {0, 0, 1, 0}); - countryGroupAssignment.put("FJ", new int[] {3, 1, 3, 3}); - countryGroupAssignment.put("FK", new int[] {4, 2, 2, 3}); - countryGroupAssignment.put("FM", new int[] {4, 2, 4, 0}); + countryGroupAssignment.put("FJ", new int[] {3, 0, 3, 3}); + countryGroupAssignment.put("FK", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("FM", new int[] {4, 0, 4, 0}); countryGroupAssignment.put("FO", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("FR", new int[] {1, 0, 3, 1}); - countryGroupAssignment.put("GA", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("GA", new int[] {3, 3, 2, 2}); countryGroupAssignment.put("GB", new int[] {0, 1, 3, 3}); countryGroupAssignment.put("GD", new int[] {2, 0, 4, 4}); - countryGroupAssignment.put("GE", new int[] {1, 1, 0, 3}); - countryGroupAssignment.put("GF", new int[] {1, 2, 4, 4}); - countryGroupAssignment.put("GG", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("GH", new int[] {3, 3, 3, 2}); + countryGroupAssignment.put("GE", new int[] {1, 1, 1, 4}); + countryGroupAssignment.put("GF", new int[] {2, 3, 4, 4}); + countryGroupAssignment.put("GG", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("GH", new int[] {3, 3, 2, 2}); countryGroupAssignment.put("GI", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("GL", new int[] {2, 2, 3, 4}); - countryGroupAssignment.put("GM", new int[] {4, 3, 3, 2}); - countryGroupAssignment.put("GN", new int[] {4, 4, 4, 0}); - countryGroupAssignment.put("GP", new int[] {2, 2, 1, 3}); - countryGroupAssignment.put("GQ", new int[] {4, 3, 3, 0}); - countryGroupAssignment.put("GR", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("GT", new int[] {3, 3, 3, 4}); + countryGroupAssignment.put("GL", new int[] {2, 2, 0, 2}); + countryGroupAssignment.put("GM", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("GN", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("GP", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("GQ", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("GR", new int[] {1, 1, 0, 2}); + countryGroupAssignment.put("GT", new int[] {3, 3, 3, 3}); countryGroupAssignment.put("GU", new int[] {1, 2, 4, 4}); - countryGroupAssignment.put("GW", new int[] {4, 4, 4, 0}); - countryGroupAssignment.put("GY", new int[] {3, 4, 1, 0}); - countryGroupAssignment.put("HK", new int[] {0, 1, 4, 4}); - countryGroupAssignment.put("HN", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("HR", new int[] {1, 0, 0, 2}); - countryGroupAssignment.put("HT", new int[] {3, 4, 4, 3}); - countryGroupAssignment.put("HU", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("GW", new int[] {4, 4, 4, 1}); + countryGroupAssignment.put("GY", new int[] {3, 2, 1, 1}); + countryGroupAssignment.put("HK", new int[] {0, 2, 3, 4}); + countryGroupAssignment.put("HN", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("HR", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("HT", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("HU", new int[] {0, 1, 0, 0}); countryGroupAssignment.put("ID", new int[] {3, 2, 3, 4}); - countryGroupAssignment.put("IE", new int[] {0, 0, 3, 2}); - countryGroupAssignment.put("IL", new int[] {0, 1, 2, 3}); + countryGroupAssignment.put("IE", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("IL", new int[] {0, 0, 2, 3}); countryGroupAssignment.put("IM", new int[] {0, 0, 0, 1}); countryGroupAssignment.put("IN", new int[] {2, 2, 4, 4}); - countryGroupAssignment.put("IO", new int[] {4, 4, 2, 2}); - countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("IR", new int[] {1, 0, 1, 0}); - countryGroupAssignment.put("IS", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("IT", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("IO", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 2}); + countryGroupAssignment.put("IR", new int[] {3, 0, 2, 2}); + countryGroupAssignment.put("IS", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("IT", new int[] {1, 0, 1, 2}); countryGroupAssignment.put("JE", new int[] {1, 0, 0, 1}); - countryGroupAssignment.put("JM", new int[] {3, 2, 2, 1}); - countryGroupAssignment.put("JO", new int[] {1, 1, 1, 2}); - countryGroupAssignment.put("JP", new int[] {0, 2, 2, 2}); - countryGroupAssignment.put("KE", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("KG", new int[] {1, 1, 2, 3}); - countryGroupAssignment.put("KH", new int[] {2, 0, 4, 4}); + countryGroupAssignment.put("JM", new int[] {2, 3, 3, 1}); + countryGroupAssignment.put("JO", new int[] {1, 2, 1, 2}); + countryGroupAssignment.put("JP", new int[] {0, 2, 1, 1}); + countryGroupAssignment.put("KE", new int[] {3, 4, 4, 3}); + countryGroupAssignment.put("KG", new int[] {1, 1, 2, 2}); + countryGroupAssignment.put("KH", new int[] {1, 0, 4, 4}); countryGroupAssignment.put("KI", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("KM", new int[] {4, 4, 3, 3}); - countryGroupAssignment.put("KN", new int[] {1, 0, 1, 4}); - countryGroupAssignment.put("KP", new int[] {1, 2, 0, 2}); - countryGroupAssignment.put("KR", new int[] {0, 3, 0, 2}); - countryGroupAssignment.put("KW", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("KY", new int[] {1, 1, 0, 2}); - countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("LA", new int[] {2, 1, 1, 0}); + countryGroupAssignment.put("KM", new int[] {4, 3, 2, 3}); + countryGroupAssignment.put("KN", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("KP", new int[] {4, 2, 4, 2}); + countryGroupAssignment.put("KR", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("KW", new int[] {2, 3, 1, 1}); + countryGroupAssignment.put("KY", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("LA", new int[] {2, 2, 1, 1}); countryGroupAssignment.put("LB", new int[] {3, 2, 0, 0}); - countryGroupAssignment.put("LC", new int[] {2, 1, 0, 0}); - countryGroupAssignment.put("LI", new int[] {0, 0, 2, 2}); - countryGroupAssignment.put("LK", new int[] {1, 1, 2, 2}); - countryGroupAssignment.put("LR", new int[] {3, 4, 4, 1}); + countryGroupAssignment.put("LC", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("LI", new int[] {0, 0, 2, 4}); + countryGroupAssignment.put("LK", new int[] {2, 1, 2, 3}); + countryGroupAssignment.put("LR", new int[] {3, 4, 3, 1}); countryGroupAssignment.put("LS", new int[] {3, 3, 2, 0}); countryGroupAssignment.put("LT", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("LU", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("LU", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("LV", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("LY", new int[] {4, 3, 4, 4}); - countryGroupAssignment.put("MA", new int[] {2, 1, 2, 2}); - countryGroupAssignment.put("MC", new int[] {1, 0, 1, 0}); + countryGroupAssignment.put("LY", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("MA", new int[] {2, 1, 2, 1}); + countryGroupAssignment.put("MC", new int[] {0, 0, 0, 1}); countryGroupAssignment.put("MD", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("ME", new int[] {1, 2, 2, 3}); - countryGroupAssignment.put("MF", new int[] {1, 4, 2, 1}); - countryGroupAssignment.put("MG", new int[] {3, 4, 1, 3}); - countryGroupAssignment.put("MH", new int[] {4, 0, 2, 3}); + countryGroupAssignment.put("ME", new int[] {1, 2, 1, 2}); + countryGroupAssignment.put("MF", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("MG", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("MH", new int[] {4, 0, 2, 4}); countryGroupAssignment.put("MK", new int[] {1, 0, 0, 0}); - countryGroupAssignment.put("ML", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("MM", new int[] {2, 3, 1, 2}); - countryGroupAssignment.put("MN", new int[] {2, 3, 2, 4}); + countryGroupAssignment.put("ML", new int[] {4, 4, 2, 0}); + countryGroupAssignment.put("MM", new int[] {3, 3, 1, 2}); + countryGroupAssignment.put("MN", new int[] {2, 3, 2, 3}); countryGroupAssignment.put("MO", new int[] {0, 0, 4, 4}); countryGroupAssignment.put("MP", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("MQ", new int[] {1, 1, 1, 3}); - countryGroupAssignment.put("MR", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("MS", new int[] {1, 4, 0, 3}); + countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("MR", new int[] {4, 2, 4, 2}); + countryGroupAssignment.put("MS", new int[] {1, 2, 3, 3}); countryGroupAssignment.put("MT", new int[] {0, 1, 0, 0}); countryGroupAssignment.put("MU", new int[] {2, 2, 3, 4}); - countryGroupAssignment.put("MV", new int[] {3, 2, 1, 1}); - countryGroupAssignment.put("MW", new int[] {4, 2, 1, 1}); - countryGroupAssignment.put("MX", new int[] {2, 4, 3, 2}); - countryGroupAssignment.put("MY", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("MZ", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("NA", new int[] {3, 2, 2, 1}); - countryGroupAssignment.put("NC", new int[] {2, 1, 3, 2}); + countryGroupAssignment.put("MV", new int[] {4, 3, 0, 2}); + countryGroupAssignment.put("MW", new int[] {3, 2, 1, 0}); + countryGroupAssignment.put("MX", new int[] {2, 4, 4, 3}); + countryGroupAssignment.put("MY", new int[] {2, 2, 3, 3}); + countryGroupAssignment.put("MZ", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("NA", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("NC", new int[] {2, 0, 3, 3}); countryGroupAssignment.put("NE", new int[] {4, 4, 4, 3}); countryGroupAssignment.put("NF", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("NG", new int[] {3, 4, 3, 2}); - countryGroupAssignment.put("NI", new int[] {3, 3, 3, 4}); - countryGroupAssignment.put("NL", new int[] {0, 2, 4, 3}); - countryGroupAssignment.put("NO", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("NP", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("NR", new int[] {4, 0, 4, 0}); - countryGroupAssignment.put("NU", new int[] {2, 2, 2, 1}); - countryGroupAssignment.put("NZ", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("OM", new int[] {2, 2, 1, 3}); + countryGroupAssignment.put("NG", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("NI", new int[] {3, 3, 4, 4}); + countryGroupAssignment.put("NL", new int[] {0, 2, 3, 3}); + countryGroupAssignment.put("NO", new int[] {0, 1, 1, 0}); + countryGroupAssignment.put("NP", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("NR", new int[] {4, 0, 3, 1}); + countryGroupAssignment.put("NZ", new int[] {0, 0, 1, 2}); + countryGroupAssignment.put("OM", new int[] {3, 2, 1, 3}); countryGroupAssignment.put("PA", new int[] {1, 3, 3, 4}); countryGroupAssignment.put("PE", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("PF", new int[] {3, 1, 0, 1}); - countryGroupAssignment.put("PG", new int[] {4, 3, 1, 1}); - countryGroupAssignment.put("PH", new int[] {3, 0, 4, 4}); + countryGroupAssignment.put("PF", new int[] {2, 2, 0, 1}); + countryGroupAssignment.put("PG", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("PH", new int[] {3, 0, 3, 4}); countryGroupAssignment.put("PK", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("PL", new int[] {1, 1, 1, 3}); - countryGroupAssignment.put("PM", new int[] {0, 2, 0, 0}); - countryGroupAssignment.put("PR", new int[] {2, 1, 3, 3}); - countryGroupAssignment.put("PS", new int[] {3, 3, 1, 4}); - countryGroupAssignment.put("PT", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("PW", new int[] {2, 2, 1, 1}); - countryGroupAssignment.put("PY", new int[] {3, 1, 3, 3}); - countryGroupAssignment.put("QA", new int[] {2, 3, 0, 1}); + countryGroupAssignment.put("PL", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("PM", new int[] {0, 2, 2, 0}); + countryGroupAssignment.put("PR", new int[] {1, 2, 3, 3}); + countryGroupAssignment.put("PS", new int[] {3, 3, 2, 4}); + countryGroupAssignment.put("PT", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("PW", new int[] {2, 1, 2, 0}); + countryGroupAssignment.put("PY", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("QA", new int[] {2, 2, 1, 2}); countryGroupAssignment.put("RE", new int[] {1, 0, 2, 2}); countryGroupAssignment.put("RO", new int[] {0, 1, 1, 2}); countryGroupAssignment.put("RS", new int[] {1, 2, 0, 0}); countryGroupAssignment.put("RU", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("RW", new int[] {3, 4, 2, 4}); - countryGroupAssignment.put("SA", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("RW", new int[] {4, 4, 2, 4}); + countryGroupAssignment.put("SA", new int[] {2, 2, 2, 1}); countryGroupAssignment.put("SB", new int[] {4, 4, 3, 0}); countryGroupAssignment.put("SC", new int[] {4, 2, 0, 1}); - countryGroupAssignment.put("SD", new int[] {4, 4, 4, 2}); + countryGroupAssignment.put("SD", new int[] {4, 4, 4, 3}); countryGroupAssignment.put("SE", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("SG", new int[] {1, 2, 3, 3}); + countryGroupAssignment.put("SG", new int[] {0, 2, 3, 3}); countryGroupAssignment.put("SH", new int[] {4, 4, 2, 3}); - countryGroupAssignment.put("SI", new int[] {0, 1, 0, 1}); - countryGroupAssignment.put("SJ", new int[] {0, 0, 2, 0}); - countryGroupAssignment.put("SK", new int[] {0, 1, 0, 1}); - countryGroupAssignment.put("SL", new int[] {4, 3, 2, 4}); - countryGroupAssignment.put("SM", new int[] {0, 0, 1, 3}); - countryGroupAssignment.put("SN", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("SO", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("SR", new int[] {3, 2, 2, 4}); - countryGroupAssignment.put("SS", new int[] {4, 2, 4, 2}); + countryGroupAssignment.put("SI", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("SJ", new int[] {2, 0, 2, 4}); + countryGroupAssignment.put("SK", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SL", new int[] {4, 3, 3, 3}); + countryGroupAssignment.put("SM", new int[] {0, 0, 2, 4}); + countryGroupAssignment.put("SN", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("SO", new int[] {3, 4, 4, 3}); + countryGroupAssignment.put("SR", new int[] {2, 2, 1, 0}); + countryGroupAssignment.put("SS", new int[] {4, 3, 4, 3}); countryGroupAssignment.put("ST", new int[] {3, 4, 2, 2}); countryGroupAssignment.put("SV", new int[] {2, 3, 3, 4}); countryGroupAssignment.put("SX", new int[] {2, 4, 1, 0}); - countryGroupAssignment.put("SY", new int[] {4, 4, 1, 0}); - countryGroupAssignment.put("SZ", new int[] {3, 4, 2, 3}); - countryGroupAssignment.put("TC", new int[] {1, 1, 3, 1}); - countryGroupAssignment.put("TD", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("SY", new int[] {4, 3, 2, 1}); + countryGroupAssignment.put("SZ", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("TC", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("TD", new int[] {4, 4, 4, 2}); countryGroupAssignment.put("TG", new int[] {3, 3, 1, 0}); countryGroupAssignment.put("TH", new int[] {1, 3, 4, 4}); countryGroupAssignment.put("TJ", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("TL", new int[] {4, 2, 4, 4}); - countryGroupAssignment.put("TM", new int[] {4, 1, 2, 3}); - countryGroupAssignment.put("TN", new int[] {2, 1, 1, 1}); + countryGroupAssignment.put("TM", new int[] {4, 1, 2, 2}); + countryGroupAssignment.put("TN", new int[] {2, 2, 1, 2}); countryGroupAssignment.put("TO", new int[] {3, 3, 3, 1}); - countryGroupAssignment.put("TR", new int[] {1, 2, 0, 1}); - countryGroupAssignment.put("TT", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("TR", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("TT", new int[] {1, 3, 1, 2}); countryGroupAssignment.put("TV", new int[] {4, 2, 2, 4}); - countryGroupAssignment.put("TW", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("TW", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("TZ", new int[] {3, 3, 4, 3}); countryGroupAssignment.put("UA", new int[] {0, 2, 1, 2}); - countryGroupAssignment.put("UG", new int[] {4, 3, 2, 3}); - countryGroupAssignment.put("US", new int[] {0, 1, 3, 3}); - countryGroupAssignment.put("UY", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("UZ", new int[] {3, 2, 2, 2}); - countryGroupAssignment.put("VA", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("VC", new int[] {2, 1, 0, 0}); + countryGroupAssignment.put("UG", new int[] {4, 3, 3, 2}); + countryGroupAssignment.put("US", new int[] {1, 1, 3, 3}); + countryGroupAssignment.put("UY", new int[] {2, 2, 1, 1}); + countryGroupAssignment.put("UZ", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("VA", new int[] {1, 2, 4, 2}); + countryGroupAssignment.put("VC", new int[] {2, 0, 2, 4}); countryGroupAssignment.put("VE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("VG", new int[] {2, 1, 1, 2}); - countryGroupAssignment.put("VI", new int[] {1, 0, 2, 4}); + countryGroupAssignment.put("VG", new int[] {3, 0, 1, 3}); + countryGroupAssignment.put("VI", new int[] {1, 1, 4, 4}); countryGroupAssignment.put("VN", new int[] {0, 2, 4, 4}); countryGroupAssignment.put("VU", new int[] {4, 1, 3, 1}); - countryGroupAssignment.put("WS", new int[] {3, 2, 3, 1}); + countryGroupAssignment.put("WS", new int[] {3, 3, 3, 2}); countryGroupAssignment.put("XK", new int[] {1, 2, 1, 0}); - countryGroupAssignment.put("YE", new int[] {4, 4, 4, 2}); - countryGroupAssignment.put("YT", new int[] {2, 0, 2, 3}); - countryGroupAssignment.put("ZA", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("ZM", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("ZW", new int[] {3, 3, 3, 1}); + countryGroupAssignment.put("YE", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("YT", new int[] {2, 2, 2, 3}); + countryGroupAssignment.put("ZA", new int[] {2, 4, 2, 2}); + countryGroupAssignment.put("ZM", new int[] {3, 2, 2, 1}); + countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 1}); return Collections.unmodifiableMap(countryGroupAssignment); } } From 5407c31726ee37c9f158f549bcdf578cfb4e15d1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 13:28:23 +0000 Subject: [PATCH 666/807] Remove WebvttSubtitle from null-checking blacklist PiperOrigin-RevId: 277916113 --- .../text/webvtt/WebvttSubtitle.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 5d3339c85a..2833ff2d0b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -73,15 +72,12 @@ import java.util.List; @Override public List getCues(long timeUs) { - ArrayList list = null; + List list = new ArrayList<>(); WebvttCue firstNormalCue = null; SpannableStringBuilder normalCueTextBuilder = null; for (int i = 0; i < numCues; i++) { if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { - if (list == null) { - list = new ArrayList<>(); - } WebvttCue cue = cues.get(i); // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping // individual cues, but tweaking their `line` value): @@ -94,9 +90,12 @@ import java.util.List; firstNormalCue = cue; } else if (normalCueTextBuilder == null) { normalCueTextBuilder = new SpannableStringBuilder(); - normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); + normalCueTextBuilder + .append(Assertions.checkNotNull(firstNormalCue.text)) + .append("\n") + .append(Assertions.checkNotNull(cue.text)); } else { - normalCueTextBuilder.append("\n").append(cue.text); + normalCueTextBuilder.append("\n").append(Assertions.checkNotNull(cue.text)); } } else { list.add(cue); @@ -110,12 +109,7 @@ import java.util.List; // there was only a single normal cue, so just add it to the list list.add(firstNormalCue); } - - if (list != null) { - return list; - } else { - return Collections.emptyList(); - } + return list; } } From 2139973e2c83cf930e80769da194e2f3f058d45b Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 13:30:21 +0000 Subject: [PATCH 667/807] Remove WebvttParserUtil from null-checking blacklist PiperOrigin-RevId: 277916279 --- .../google/android/exoplayer2/text/webvtt/WebvttParserUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index 1674e9345c..dce8f8157f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.webvtt; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -98,6 +99,7 @@ public final class WebvttParserUtil { * reached without a cue header being found. In the case that a cue header is found, groups 1, * 2 and 3 of the returned matcher contain the start time, end time and settings list. */ + @Nullable public static Matcher findNextCueHeader(ParsableByteArray input) { String line; while ((line = input.readLine()) != null) { From 129efa2ebf56776512d0d70d116c9884a1b66743 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 13:32:20 +0000 Subject: [PATCH 668/807] Remove WebvttCssStyle from null-checking blacklist PiperOrigin-RevId: 277916508 --- .../text/webvtt/WebvttCssStyle.java | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 2bd1af01e8..9186455702 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; import android.text.Layout; +import android.text.TextUtils; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -25,6 +27,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Style object of a Css style block in a Webvtt file. @@ -80,7 +83,7 @@ public final class WebvttCssStyle { private String targetVoice; // Style properties. - private String fontFamily; + @Nullable private String fontFamily; private int fontColor; private boolean hasFontColor; private int backgroundColor; @@ -91,12 +94,16 @@ public final class WebvttCssStyle { @OptionalBoolean private int italic; @FontSizeUnit private int fontSizeUnit; private float fontSize; - private Layout.Alignment textAlign; + @Nullable private Layout.Alignment textAlign; + // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed + // because reset() only assigns fields, it doesn't read any. + @SuppressWarnings("nullness:method.invocation.invalid") public WebvttCssStyle() { reset(); } + @EnsuresNonNull({"targetId", "targetTag", "targetClasses", "targetVoice"}) public void reset() { targetId = ""; targetTag = ""; @@ -133,14 +140,13 @@ public final class WebvttCssStyle { * Returns a value in a score system compliant with the CSS Specificity rules. * * @see CSS Cascading - * - * The score works as follows: - *

          - *
        • Id match adds 0x40000000 to the score. - *
        • Each class and voice match adds 4 to the score. - *
        • Tag matching adds 2 to the score. - *
        • Universal selector matching scores 1. - *
        + *

        The score works as follows: + *

          + *
        • Id match adds 0x40000000 to the score. + *
        • Each class and voice match adds 4 to the score. + *
        • Tag matching adds 2 to the score. + *
        • Universal selector matching scores 1. + *
        * * @param id The id of the cue if present, {@code null} otherwise. * @param tag Name of the tag, {@code null} if it refers to the entire cue. @@ -148,12 +154,13 @@ public final class WebvttCssStyle { * @param voice Annotated voice if present, {@code null} otherwise. * @return The score of the match, zero if there is no match. */ - public int getSpecificityScore(String id, String tag, String[] classes, String voice) { + public int getSpecificityScore( + @Nullable String id, @Nullable String tag, String[] classes, @Nullable String voice) { if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty() && targetVoice.isEmpty()) { // The selector is universal. It matches with the minimum score if and only if the given // element is a whole cue. - return tag.isEmpty() ? 1 : 0; + return TextUtils.isEmpty(tag) ? 1 : 0; } int score = 0; score = updateScoreForMatch(score, targetId, id, 0x40000000); @@ -208,6 +215,7 @@ public final class WebvttCssStyle { return this; } + @Nullable public String getFontFamily() { return fontFamily; } @@ -251,6 +259,7 @@ public final class WebvttCssStyle { return hasBackgroundColor; } + @Nullable public Layout.Alignment getTextAlign() { return textAlign; } @@ -309,8 +318,8 @@ public final class WebvttCssStyle { } } - private static int updateScoreForMatch(int currentScore, String target, String actual, - int score) { + private static int updateScoreForMatch( + int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { return currentScore; } From 616f4774e1e8dbcbf753c18b6448b0d9d824b4e2 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 13:33:20 +0000 Subject: [PATCH 669/807] Remove WebvttCueParser from null-checking blacklist PiperOrigin-RevId: 277916639 --- .../text/webvtt/WebvttCueParser.java | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index d8215e9847..f587d70e90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; +import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -30,13 +31,14 @@ import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +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.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; @@ -155,7 +157,7 @@ public final class WebvttCueParser { * @param builder Output builder. */ /* package */ static void parseCueText( - String id, String markup, WebvttCue.Builder builder, List styles) { + @Nullable String id, String markup, WebvttCue.Builder builder, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); @@ -174,8 +176,11 @@ public final class WebvttCueParser { boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH; String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1); + if (fullTagExpression.trim().isEmpty()) { + continue; + } String tagName = getTagName(fullTagExpression); - if (tagName == null || !isSupportedTag(tagName)) { + if (!isSupportedTag(tagName)) { continue; } if (isClosingTag) { @@ -223,8 +228,13 @@ public final class WebvttCueParser { builder.setText(spannedText); } - private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, - WebvttCue.Builder builder, StringBuilder textBuilder, List styles) { + private static boolean parseCue( + @Nullable String id, + Matcher cueHeaderMatcher, + ParsableByteArray webvttData, + WebvttCue.Builder builder, + StringBuilder textBuilder, + List styles) { try { // Parse the cue start and end times. builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) @@ -238,8 +248,9 @@ public final class WebvttCueParser { // Parse the cue text. textBuilder.setLength(0); - String line; - while (!TextUtils.isEmpty(line = webvttData.readLine())) { + for (String line = webvttData.readLine(); + !TextUtils.isEmpty(line); + line = webvttData.readLine()) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } @@ -362,8 +373,12 @@ public final class WebvttCueParser { } } - private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text, - List styles, List scratchStyleMatches) { + private static void applySpansForTag( + @Nullable String cueId, + StartTag startTag, + SpannableStringBuilder text, + List styles, + List scratchStyleMatches) { int start = startTag.position; int end = text.length(); switch(startTag.name) { @@ -421,9 +436,10 @@ public final class WebvttCueParser { spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (style.getTextAlign() != null) { - spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + Layout.Alignment textAlign = style.getTextAlign(); + if (textAlign != null) { + spannedText.setSpan( + new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: @@ -452,14 +468,15 @@ public final class WebvttCueParser { */ private static String getTagName(String tagExpression) { tagExpression = tagExpression.trim(); - if (tagExpression.isEmpty()) { - return null; - } + Assertions.checkArgument(!tagExpression.isEmpty()); return Util.splitAtFirst(tagExpression, "[ \\.]")[0]; } - private static void getApplicableStyles(List declaredStyles, String id, - StartTag tag, List output) { + private static void getApplicableStyles( + List declaredStyles, + @Nullable String id, + StartTag tag, + List output) { int styleCount = declaredStyles.size(); for (int i = 0; i < styleCount; i++) { WebvttCssStyle style = declaredStyles.get(i); @@ -506,9 +523,7 @@ public final class WebvttCueParser { public static StartTag buildStartTag(String fullTagExpression, int position) { fullTagExpression = fullTagExpression.trim(); - if (fullTagExpression.isEmpty()) { - return null; - } + Assertions.checkArgument(!fullTagExpression.isEmpty()); int voiceStartIndex = fullTagExpression.indexOf(" "); String voice; if (voiceStartIndex == -1) { @@ -521,7 +536,7 @@ public final class WebvttCueParser { String name = nameAndClasses[0]; String[] classes; if (nameAndClasses.length > 1) { - classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length); + classes = Util.nullSafeArrayCopyOfRange(nameAndClasses, 1, nameAndClasses.length); } else { classes = NO_CLASSES; } From 2106e5f328971fcdd2c4de2c40a0263391b64187 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 13:34:15 +0000 Subject: [PATCH 670/807] Annotate webvtt package with @NonNullApi PiperOrigin-RevId: 277916734 --- .../exoplayer2/text/webvtt/package-info.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java new file mode 100644 index 0000000000..ee429b5261 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java @@ -0,0 +1,20 @@ +/* + * 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.text.webvtt; + +import com.google.android.exoplayer2.util.NonNullApi; From a4e7274cca0ab57f0b23bfa0251a06fae243bf94 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 1 Nov 2019 14:38:22 +0000 Subject: [PATCH 671/807] Update audio extension build configurations - Fix FLAC extension build (currently broken due to use of std::array, but fixed by migrating to NDK r20). - Move opus and ffmpeg extensions to NDK r20. For ffmpeg, upgrade to release 4.2 which requires using libswresample and updates to the build script. Issue: #6601 PiperOrigin-RevId: 277924119 --- RELEASENOTES.md | 6 ++++++ extensions/ffmpeg/README.md | 4 ++-- .../exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 2 +- extensions/ffmpeg/src/main/jni/Android.mk | 11 +++++++--- extensions/ffmpeg/src/main/jni/Application.mk | 2 +- .../ffmpeg/src/main/jni/build_ffmpeg.sh | 20 +++++++++++-------- extensions/flac/README.md | 4 ++-- extensions/flac/src/main/jni/Application.mk | 2 +- extensions/opus/README.md | 3 ++- extensions/opus/src/main/jni/Application.mk | 2 +- 10 files changed, 36 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e2713f16bf..b1e054a0bf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -107,6 +107,12 @@ * Fix the start of audio getting truncated when transitioning to a new item in a playlist of opus streams. * Fix detection of Dolby Atmos in HLS to match the HLS authoring specification. +* Fix FLAC extension build + ([#6601](https://github.com/google/ExoPlayer/issues/6601). +* Update the ffmpeg, flac and opus extension build instructions to use NDK r20. +* Update the ffmpeg extension to release 4.2. It is necessary to rebuild the + native part of the extension after this change, following the instructions in + the extension's readme. ### 2.10.6 (2019-10-17) ### diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index cc9ea2a8c7..f8120ed11b 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -29,7 +29,7 @@ FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" ``` * Download the [Android NDK][] and set its location in a shell variable. - Only versions up to NDK 15c are supported currently. + This build configuration has been tested on NDK r20. ``` NDK_PATH="" @@ -50,7 +50,7 @@ ENABLED_DECODERS=(vorbis opus flac) ``` * Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will - fetch and build FFmpeg release 4.0 for armeabi-v7a, arm64-v8a and x86: + fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86: ``` cd "${FFMPEG_EXT_PATH}" && \ diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 58109c1666..5b816b8c20 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -34,7 +34,7 @@ public final class FfmpegLibrary { private static final String TAG = "FfmpegLibrary"; private static final LibraryLoader LOADER = - new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); + new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg"); private FfmpegLibrary() {} diff --git a/extensions/ffmpeg/src/main/jni/Android.mk b/extensions/ffmpeg/src/main/jni/Android.mk index 046f90a5b2..22a4edcdae 100644 --- a/extensions/ffmpeg/src/main/jni/Android.mk +++ b/extensions/ffmpeg/src/main/jni/Android.mk @@ -22,12 +22,17 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libavutil +LOCAL_MODULE := libavresample LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libavresample +LOCAL_MODULE := libswresample +LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so +include $(PREBUILT_SHARED_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libavutil LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so include $(PREBUILT_SHARED_LIBRARY) @@ -35,6 +40,6 @@ include $(CLEAR_VARS) LOCAL_MODULE := ffmpeg LOCAL_SRC_FILES := ffmpeg_jni.cc LOCAL_C_INCLUDES := ffmpeg -LOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil +LOCAL_SHARED_LIBRARIES := libavcodec libavresample libswresample libavutil LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/ffmpeg/src/main/jni/Application.mk b/extensions/ffmpeg/src/main/jni/Application.mk index 59bf5f8f87..7d6f732548 100644 --- a/extensions/ffmpeg/src/main/jni/Application.mk +++ b/extensions/ffmpeg/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 diff --git a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh index 358d2a2b8f..a76fa0e589 100755 --- a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh +++ b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh @@ -32,9 +32,10 @@ COMMON_OPTIONS=" --disable-postproc --disable-avfilter --disable-symver - --disable-swresample --enable-avresample + --enable-swresample " +TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin" for decoder in "${ENABLED_DECODERS[@]}" do COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}" @@ -42,13 +43,14 @@ done cd "${FFMPEG_EXT_PATH}" (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) cd ffmpeg -git checkout release/4.0 +git checkout release/4.2 ./configure \ --libdir=android-libs/armeabi-v7a \ --arch=arm \ --cpu=armv7-a \ - --cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-" \ - --sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \ + --cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \ + --nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \ + --strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \ --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ --extra-ldflags="-Wl,--fix-cortex-a8" \ --extra-ldexeflags=-pie \ @@ -60,8 +62,9 @@ make clean --libdir=android-libs/arm64-v8a \ --arch=aarch64 \ --cpu=armv8-a \ - --cross-prefix="${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-" \ - --sysroot="${NDK_PATH}/platforms/android-21/arch-arm64/" \ + --cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \ + --nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \ + --strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \ --extra-ldexeflags=-pie \ ${COMMON_OPTIONS} make -j4 @@ -71,8 +74,9 @@ make clean --libdir=android-libs/x86 \ --arch=x86 \ --cpu=i686 \ - --cross-prefix="${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-" \ - --sysroot="${NDK_PATH}/platforms/android-9/arch-x86/" \ + --cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \ + --nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \ + --strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \ --extra-ldexeflags=-pie \ --disable-asm \ ${COMMON_OPTIONS} diff --git a/extensions/flac/README.md b/extensions/flac/README.md index b4b0dae002..e534f9b2ac 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -28,8 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" ``` -* Download the [Android NDK][] (version <= 17c) and set its location in an - environment variable: +* Download the [Android NDK][] and set its location in an environment variable. + This build configuration has been tested on NDK r20. ``` NDK_PATH="" diff --git a/extensions/flac/src/main/jni/Application.mk b/extensions/flac/src/main/jni/Application.mk index eba20352f4..e33070e121 100644 --- a/extensions/flac/src/main/jni/Application.mk +++ b/extensions/flac/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-14 diff --git a/extensions/opus/README.md b/extensions/opus/README.md index af44e84b04..ce88f0ef7d 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" ``` -* Download the [Android NDK][] and set its location in an environment variable: +* Download the [Android NDK][] and set its location in an environment variable. + This build configuration has been tested on NDK r20. ``` NDK_PATH="" diff --git a/extensions/opus/src/main/jni/Application.mk b/extensions/opus/src/main/jni/Application.mk index 59bf5f8f87..7d6f732548 100644 --- a/extensions/opus/src/main/jni/Application.mk +++ b/extensions/opus/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 From c5c50078d77572e988a14d7afd05412b13ea3748 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 1 Nov 2019 14:44:54 +0000 Subject: [PATCH 672/807] Reset MediaSession shuffle/repeat modes if player is null - This is for consistency with PlayerControlView. - Also update PlayerNotificationManager notification if shuffle mode changes. This is for consistency with what happens when the repeat mode changes. By default the notification will be unchanged, but custom implementations can extend and then override createNotification, and given these modes change infrequently it feels like we can just do this. The alternative for achieving consistency would be to remove handling of repeat mode changes. Issue: #6582 PiperOrigin-RevId: 277925094 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 3 +++ .../android/exoplayer2/ui/PlayerNotificationManager.java | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) 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 50777dd4d6..8def61aead 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 @@ -701,6 +701,9 @@ public final class MediaSessionConnector { /* position= */ 0, /* playbackSpeed= */ 0, /* updateTime= */ SystemClock.elapsedRealtime()); + + mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE); + mediaSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE); mediaSession.setPlaybackState(builder.build()); return; } 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 fb11dfae71..9decf900a7 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 @@ -1348,7 +1348,12 @@ public class PlayerNotificationManager { } @Override - public void onRepeatModeChanged(int repeatMode) { + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + startOrUpdateNotification(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { startOrUpdateNotification(); } } From 07e93c15f9278f0e6ac72efcb18421915a21f622 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 15:02:55 +0000 Subject: [PATCH 673/807] Add support for subtitle files in the demo app issue:#5523 PiperOrigin-RevId: 277927555 --- RELEASENOTES.md | 2 + demos/main/src/main/assets/media.exolist.json | 12 +++++ .../exoplayer2/demo/PlayerActivity.java | 23 +++++++++- .../android/exoplayer2/demo/Sample.java | 44 ++++++++++++++++++- .../demo/SampleChooserActivity.java | 28 ++++++++++-- 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b1e054a0bf..3cbb45c028 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -113,6 +113,8 @@ * Update the ffmpeg extension to release 4.2. It is necessary to rebuild the native part of the extension after this change, following the instructions in the extension's readme. +* Add support for subtitle files to the demo app + ([#5523](https://github.com/google/ExoPlayer/issues/5523)). ### 2.10.6 (2019-10-17) ### diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 712c250846..57f0045791 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -582,5 +582,17 @@ "spherical_stereo_mode": "top_bottom" } ] + }, + { + "name": "Subtitles", + "samples": [ + { + "name": "TTML", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml", + "subtitle_mime_type": "application/ttml+xml", + "subtitle_language": "en" + } + ] } ] 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 74e919293d..85ff41c94d 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 @@ -34,6 +34,7 @@ import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; @@ -53,7 +54,9 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -114,8 +117,11 @@ public class PlayerActivity extends AppCompatActivity 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 TUNNELING = "tunneling"; + public static final String TUNNELING_EXTRA = "tunneling"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; + public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; + public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; // For backwards compatibility only. public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; @@ -204,7 +210,7 @@ public class PlayerActivity extends AppCompatActivity } else { DefaultTrackSelector.ParametersBuilder builder = new DefaultTrackSelector.ParametersBuilder(/* context= */ this); - boolean tunneling = intent.getBooleanExtra(TUNNELING, false); + boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false); if (Util.SDK_INT >= 21 && tunneling) { builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this)); } @@ -424,6 +430,19 @@ public class PlayerActivity extends AppCompatActivity MediaSource[] mediaSources = new MediaSource[samples.length]; for (int i = 0; i < samples.length; i++) { mediaSources[i] = createLeafMediaSource(samples[i]); + Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo; + if (subtitleInfo != null) { + Format subtitleFormat = + Format.createTextSampleFormat( + /* id= */ null, + subtitleInfo.mimeType, + /* selectionFlags= */ 0, + subtitleInfo.language); + MediaSource subtitleMediaSource = + new SingleSampleMediaSource.Factory(dataSourceFactory) + .createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET); + mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource); + } } MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); 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 index 40c20c298c..85530b993b 100644 --- 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 @@ -24,6 +24,9 @@ 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.IS_LIVE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA; +import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA; import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA; import android.content.Intent; @@ -51,7 +54,8 @@ import java.util.UUID; isLive, DrmInfo.createFromIntent(intent, extrasKeySuffix), adTagUri, - /* sphericalStereoMode= */ null); + /* sphericalStereoMode= */ null, + SubtitleInfo.createFromIntent(intent, extrasKeySuffix)); } public final Uri uri; @@ -60,6 +64,7 @@ import java.util.UUID; public final DrmInfo drmInfo; public final Uri adTagUri; @Nullable public final String sphericalStereoMode; + @Nullable SubtitleInfo subtitleInfo; public UriSample( String name, @@ -68,7 +73,8 @@ import java.util.UUID; boolean isLive, DrmInfo drmInfo, Uri adTagUri, - @Nullable String sphericalStereoMode) { + @Nullable String sphericalStereoMode, + @Nullable SubtitleInfo subtitleInfo) { super(name); this.uri = uri; this.extension = extension; @@ -76,6 +82,7 @@ import java.util.UUID; this.drmInfo = drmInfo; this.adTagUri = adTagUri; this.sphericalStereoMode = sphericalStereoMode; + this.subtitleInfo = subtitleInfo; } @Override @@ -100,6 +107,9 @@ import java.util.UUID; if (drmInfo != null) { drmInfo.addToIntent(intent, extrasKeySuffix); } + if (subtitleInfo != null) { + subtitleInfo.addToIntent(intent, extrasKeySuffix); + } } } @@ -167,6 +177,36 @@ import java.util.UUID; } } + public static final class SubtitleInfo { + + @Nullable + public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) { + if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) { + return null; + } + return new SubtitleInfo( + Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)), + intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix), + intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix)); + } + + public final Uri uri; + public final String mimeType; + @Nullable public final String language; + + public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) { + this.uri = Assertions.checkNotNull(uri); + this.mimeType = Assertions.checkNotNull(mimeType); + this.language = language; + } + + public void addToIntent(Intent intent, String extrasKeySuffix) { + intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString()); + intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType); + intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language); + } + } + public static Sample createFromIntent(Intent intent) { if (ACTION_VIEW_LIST.equals(intent.getAction())) { ArrayList intentUris = new ArrayList<>(); 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 22533fd410..cdce29aa5e 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 @@ -178,7 +178,7 @@ public class SampleChooserActivity extends AppCompatActivity ? PlayerActivity.ABR_ALGORITHM_RANDOM : PlayerActivity.ABR_ALGORITHM_DEFAULT; intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm); - intent.putExtra(PlayerActivity.TUNNELING, isNonNullAndChecked(tunnelingMenuItem)); + intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem)); sample.addToIntent(intent); startActivity(intent); return true; @@ -311,6 +311,10 @@ public class SampleChooserActivity extends AppCompatActivity ArrayList playlistSamples = null; String adTagUri = null; String sphericalStereoMode = null; + List subtitleInfos = new ArrayList<>(); + Uri subtitleUri = null; + String subtitleMimeType = null; + String subtitleLanguage = null; reader.beginObject(); while (reader.hasNext()) { @@ -352,7 +356,7 @@ public class SampleChooserActivity extends AppCompatActivity playlistSamples = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { - playlistSamples.add((UriSample) readEntry(reader, true)); + playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true)); } reader.endArray(); break; @@ -364,6 +368,15 @@ public class SampleChooserActivity extends AppCompatActivity !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); sphericalStereoMode = reader.nextString(); break; + case "subtitle_uri": + subtitleUri = Uri.parse(reader.nextString()); + break; + case "subtitle_mime_type": + subtitleMimeType = reader.nextString(); + break; + case "subtitle_language": + subtitleLanguage = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } @@ -377,6 +390,14 @@ public class SampleChooserActivity extends AppCompatActivity drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); + Sample.SubtitleInfo subtitleInfo = + subtitleUri == null + ? null + : new Sample.SubtitleInfo( + subtitleUri, + Assertions.checkNotNull( + subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."), + subtitleLanguage); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); return new PlaylistSample(sampleName, playlistSamplesArray); @@ -388,7 +409,8 @@ public class SampleChooserActivity extends AppCompatActivity isLive, drmInfo, adTagUri != null ? Uri.parse(adTagUri) : null, - sphericalStereoMode); + sphericalStereoMode, + subtitleInfo); } } From 8dcd1e53bcc9248b1d1fa05ac91896a738cfdb55 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 1 Nov 2019 15:10:25 +0000 Subject: [PATCH 674/807] Remove or suppress warnings where we use our own deprecated APIs PiperOrigin-RevId: 277928790 --- .../com/google/android/exoplayer2/ExoPlayerFactory.java | 1 + .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 1 + .../exoplayer2/offline/DownloaderConstructorHelper.java | 7 ++++--- .../android/exoplayer2/upstream/DefaultDataSource.java | 1 - .../android/exoplayer2/upstream/DefaultHttpDataSource.java | 5 +++-- 5 files changed, 9 insertions(+), 6 deletions(-) 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 efe351c70a..e4f239df77 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 @@ -278,6 +278,7 @@ public final class ExoPlayerFactory { * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link * MediaSource} factories. */ + @SuppressWarnings("deprecation") @Deprecated public static SimpleExoPlayer newSimpleInstance( Context context, 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 68b3e241fe..06aea69035 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 @@ -253,6 +253,7 @@ public class DefaultDrmSessionManager * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. * @deprecated Use {@link Builder} instead. */ + @SuppressWarnings("deprecation") @Deprecated public DefaultDrmSessionManager( UUID uuid, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index cd090c2c5e..0d53b3cde0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.FileDataSource; -import com.google.android.exoplayer2.upstream.FileDataSourceFactory; import com.google.android.exoplayer2.upstream.PriorityDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSink; @@ -60,7 +59,8 @@ public final class DownloaderConstructorHelper { * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for * downloading data. * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s - * for reading data from the cache. If null then a {@link FileDataSourceFactory} will be used. + * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be + * used. * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, @@ -87,7 +87,8 @@ public final class DownloaderConstructorHelper { * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for * downloading data. * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s - * for reading data from the cache. If null then a {@link FileDataSourceFactory} will be used. + * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be + * used. * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, 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 dec035c12e..98026c4677 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 @@ -113,7 +113,6 @@ public final class DefaultDataSource implements DataSource { context, new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, 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 436cad0d64..ae115ab58c 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 @@ -49,8 +49,8 @@ import java.util.zip.GZIPInputStream; * *

        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, int, int, boolean, RequestProperties)} constructor and - * passing {@code true} as the second last argument. + * #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} constructor and passing + * {@code true} for the {@code allowCrossProtocolRedirects} argument. * *

        Note: HTTP request headers will be set using all parameters passed via (in order of decreasing * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to @@ -171,6 +171,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link * #setContentTypePredicate(Predicate)}. */ + @SuppressWarnings("deprecation") @Deprecated public DefaultHttpDataSource( String userAgent, From 15f8d8668e11ab14a81c54c9a835b2b5354cc430 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 15:48:03 +0000 Subject: [PATCH 675/807] Select exolist-specified subtitles by default If a sample has a subtitle file listed, it makes sense to show it by default. PiperOrigin-RevId: 277934597 --- .../java/com/google/android/exoplayer2/demo/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 85ff41c94d..2f8d0045d3 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 @@ -436,7 +436,7 @@ public class PlayerActivity extends AppCompatActivity Format.createTextSampleFormat( /* id= */ null, subtitleInfo.mimeType, - /* selectionFlags= */ 0, + C.SELECTION_FLAG_DEFAULT, subtitleInfo.language); MediaSource subtitleMediaSource = new SingleSampleMediaSource.Factory(dataSourceFactory) From 5d46d4f74fbc804443f955e321ce535629cca632 Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 1 Nov 2019 18:12:41 +0000 Subject: [PATCH 676/807] Add parameter names to Format creation PiperOrigin-RevId: 277963928 --- .../exoplayer2/extractor/ogg/FlacReader.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 4efd5c5e11..d6f3999c35 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 @@ -78,17 +78,17 @@ import java.util.List; List initializationData = Collections.singletonList(metadata); setupData.format = Format.createAudioSampleFormat( - null, + /* id= */ null, MimeTypes.AUDIO_FLAC, - null, - Format.NO_VALUE, - streamMetadata.bitRate(), + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ streamMetadata.bitRate(), streamMetadata.channels, streamMetadata.sampleRate, initializationData, - null, - 0, - null); + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { flacOggSeeker = new FlacOggSeeker(); flacOggSeeker.parseSeekTable(packet); From 922991da88cb06aac4d68ebcaf9809fc7bb7e63b Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Nov 2019 18:38:37 +0000 Subject: [PATCH 677/807] Add @NonNullApi to text packages with no blacklisted files PiperOrigin-RevId: 277969385 --- .../exoplayer2/text/pgs/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/text/subrip/package-info.java | 19 +++++++++++++++++++ .../exoplayer2/text/tx3g/package-info.java | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java new file mode 100644 index 0000000000..ff0819d99a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/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.text.pgs; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java new file mode 100644 index 0000000000..bb7565c075 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/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.text.subrip; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java new file mode 100644 index 0000000000..2ae99adf58 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/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.text.tx3g; + +import com.google.android.exoplayer2.util.NonNullApi; From 7e070683a38c96e2f55a305650765e300d8a6121 Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 2 Nov 2019 03:46:19 +0000 Subject: [PATCH 678/807] Expose getMetrics() in ExoV1 and ExoV2 FrameworkMediaDrm classes. PiperOrigin-RevId: 278054214 --- .../android/exoplayer2/drm/DummyExoMediaDrm.java | 7 +++++++ .../google/android/exoplayer2/drm/ExoMediaDrm.java | 9 +++++++++ .../android/exoplayer2/drm/FrameworkMediaDrm.java | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java index f7b24cf251..b619d9486f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.drm; import android.media.MediaDrmException; +import android.os.PersistableBundle; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.Util; @@ -104,6 +105,12 @@ public final class DummyExoMediaDrm implements ExoMedi throw new IllegalStateException(); } + @Override + @Nullable + public PersistableBundle getMetrics() { + return null; + } + @Override public String getPropertyString(String propertyName) { return ""; 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 9846a76328..1d0e15e81c 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 @@ -21,6 +21,7 @@ import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.os.Handler; +import android.os.PersistableBundle; import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.util.HashMap; @@ -287,6 +288,14 @@ public interface ExoMediaDrm { */ void restoreKeys(byte[] sessionId, byte[] keySetId); + /** + * Returns drm metrics. May be null if unavailable. + * + * @see MediaDrm#getMetrics() + */ + @Nullable + PersistableBundle getMetrics(); + /** * @see MediaDrm#getPropertyString(String) */ 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 e7853e0a0b..d040552638 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 @@ -23,6 +23,7 @@ import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; +import android.os.PersistableBundle; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -225,6 +226,16 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Sun, 3 Nov 2019 19:44:46 +0000 Subject: [PATCH 679/807] Suppress warnings emitted by Checker Framework version 2.11.1 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: TAP train for global presubmit queue http://test/OCL:278152710:BASE:278144052:1572760370662:22459c12 PiperOrigin-RevId: 278241536 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 2 ++ .../exoplayer2/audio/ChannelMappingAudioProcessor.java | 5 ++++- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 ++ .../com/google/android/exoplayer2/extractor/mp4/Track.java | 2 ++ .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 4 ++++ .../android/exoplayer2/offline/DefaultDownloadIndex.java | 2 ++ .../android/exoplayer2/source/SingleSampleMediaPeriod.java | 2 ++ .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 7 ++++++- .../android/exoplayer2/upstream/DataSchemeDataSource.java | 2 ++ .../android/exoplayer2/ui/spherical/SceneRenderer.java | 2 ++ 10 files changed, 28 insertions(+), 2 deletions(-) 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 8def61aead..baf5154e6c 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 @@ -647,6 +647,8 @@ public final class MediaSessionConnector { * @param customActionProviders The custom action providers, or null to remove all existing custom * action providers. */ + // incompatible types in assignment. + @SuppressWarnings("nullness:assignment.type.incompatible") public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) { this.customActionProviders = customActionProviders == null ? new CustomActionProvider[0] : customActionProviders; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index ea155323bb..6b84662093 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -25,7 +25,10 @@ import java.util.Arrays; * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ -/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { +/* package */ +// the constructor does not initialize fields: pendingOutputChannels, outputChannels +@SuppressWarnings("nullness:initialization.fields.uninitialized") +final class ChannelMappingAudioProcessor extends BaseAudioProcessor { @Nullable private int[] pendingOutputChannels; 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 06aea69035..f9606d591e 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 @@ -326,6 +326,8 @@ public class DefaultDrmSessionManager new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } + // the constructor does not initialize fields: offlineLicenseKeySetId + @SuppressWarnings("nullness:initialization.fields.uninitialized") private DefaultDrmSessionManager( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider, 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 7676926c4d..0a21ddd3a3 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 @@ -129,6 +129,8 @@ public final class Track { : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; } + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") public Track copyWithFormat(Format format) { return new Track( id, 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 07836672e5..9adb6bc7bc 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 @@ -943,6 +943,8 @@ public final class MediaCodecUtil { @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos; + // the constructor does not initialize fields: mediaCodecInfos + @SuppressWarnings("nullness:initialization.fields.uninitialized") public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) { codecKind = includeSecure || includeTunneling @@ -956,6 +958,8 @@ public final class MediaCodecUtil { return mediaCodecInfos.length; } + // incompatible types in return. + @SuppressWarnings("nullness:return.type.incompatible") @Override public android.media.MediaCodecInfo getCodecInfoAt(int index) { ensureMediaCodecInfosInitialized(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index ef4bd00f20..4517e4ee9a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -302,6 +302,8 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") private Cursor getCursor(String selection, @Nullable String[] selectionArgs) throws DatabaseIOException { try { 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 a5d8266ef6..ca50c342b5 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 @@ -383,6 +383,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private byte[] sampleData; + // the constructor does not initialize fields: sampleData + @SuppressWarnings("nullness:initialization.fields.uninitialized") public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { this.dataSpec = dataSpec; this.dataSource = new StatsDataSource(dataSource); 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 eae21b5b30..041435482c 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 @@ -609,13 +609,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Nullable private long[][] allocationCheckpoints; - /* package */ DefaultBandwidthProvider( + /* package */ + // the constructor does not initialize fields: allocationCheckpoints + @SuppressWarnings("nullness:initialization.fields.uninitialized") + DefaultBandwidthProvider( BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { this.bandwidthMeter = bandwidthMeter; this.bandwidthFraction = bandwidthFraction; this.reservedBandwidth = reservedBandwidth; } + // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0] + @SuppressWarnings("nullness:unboxing.of.nullable") @Override public long getAllocatedBandwidth() { long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); 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 55c580ead2..e592c3bec3 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 @@ -36,6 +36,8 @@ public final class DataSchemeDataSource extends BaseDataSource { private int endPosition; private int readPosition; + // the constructor does not initialize fields: data + @SuppressWarnings("nullness:initialization.fields.uninitialized") public DataSchemeDataSource() { super(/* isNetwork= */ false); } 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 5080e86345..01fa6837ea 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 @@ -60,6 +60,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Methods called on any thread. + // the constructor does not initialize fields: lastProjectionData + @SuppressWarnings("nullness:initialization.fields.uninitialized") public SceneRenderer() { frameAvailable = new AtomicBoolean(); resetRotationAtNextFrame = new AtomicBoolean(true); From bd61b63ebc5bc8beb41527576495eb0b327fa497 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 4 Nov 2019 09:36:57 +0000 Subject: [PATCH 680/807] Remove unnecessary exceptions in method signature PiperOrigin-RevId: 278327151 --- .../google/android/exoplayer2/extractor/ogg/FlacReader.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 d6f3999c35..6f64112e4c 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 @@ -68,8 +68,7 @@ import java.util.List; } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { byte[] data = packet.data; if (streamMetadata == null) { streamMetadata = new FlacStreamMetadata(data, 17); From 46d58b5edab2a8840f4955c0a02aa9e670b536f2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Nov 2019 10:09:08 +0000 Subject: [PATCH 681/807] Add missing IntDef case in switch PiperOrigin-RevId: 278332276 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 1 + 1 file changed, 1 insertion(+) 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 baf5154e6c..cce2ebfc28 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 @@ -938,6 +938,7 @@ public final class MediaSessionConnector { return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; case Player.STATE_ENDED: return PlaybackStateCompat.STATE_STOPPED; + case Player.STATE_IDLE: default: return PlaybackStateCompat.STATE_NONE; } From 165ff55502d52e99586f0d7ed72964617e7e3540 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 4 Nov 2019 10:10:57 +0000 Subject: [PATCH 682/807] Fix inverted arguments in FlacReader format creation PiperOrigin-RevId: 278332587 --- .../google/android/exoplayer2/extractor/ogg/FlacReader.java | 4 ++-- library/core/src/test/assets/ogg/bear_flac.ogg.0.dump | 4 ++-- library/core/src/test/assets/ogg/bear_flac.ogg.1.dump | 4 ++-- library/core/src/test/assets/ogg/bear_flac.ogg.2.dump | 4 ++-- library/core/src/test/assets/ogg/bear_flac.ogg.3.dump | 4 ++-- library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump | 4 ++-- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump | 4 ++-- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump | 4 ++-- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump | 4 ++-- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump | 4 ++-- .../src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) 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 6f64112e4c..cc536acb14 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 @@ -80,8 +80,8 @@ import java.util.List; /* id= */ null, MimeTypes.AUDIO_FLAC, /* codecs= */ null, - /* bitrate= */ Format.NO_VALUE, - /* maxInputSize= */ streamMetadata.bitRate(), + streamMetadata.bitRate(), + /* maxInputSize= */ Format.NO_VALUE, streamMetadata.channels, streamMetadata.sampleRate, initializationData, diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump index dbe97c02bd..5b8d893f1a 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump index d1246a3e64..fff76c5b05 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump index ec0336309a..b4d3534161 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump index 1e3254a9fc..27c29cba58 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump index dbe97c02bd..5b8d893f1a 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump index cce7bf2450..2ecdc9784c 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump index ac36a48412..0ed2a86b9e 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump index dae0d878fa..229e90584e 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump index c9570ab58e..89c6d178ff 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index 7a3e7ef5ac..7a4ba81f23 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -5,11 +5,11 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = -1 + bitrate = 768000 id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = 768000 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 From 5968c8345b9db295adfea6948f43bbbc2b0c4ac8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Nov 2019 17:35:04 +0000 Subject: [PATCH 683/807] Remove auto-value dependency PiperOrigin-RevId: 278398045 --- library/core/build.gradle | 4 ---- testutils/build.gradle | 2 -- 2 files changed, 6 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index afccef3e24..33798c0c30 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -65,18 +65,14 @@ dependencies { androidTestImplementation 'androidx.test:runner:' + androidxTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion - androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion - androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion testImplementation 'androidx.test:core:' + androidxTestVersion testImplementation 'androidx.test.ext:junit:' + androidxTestVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion - testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion - testAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion } ext { diff --git a/testutils/build.gradle b/testutils/build.gradle index 3c7b13a6a8..0d6439cf6c 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -44,8 +44,6 @@ dependencies { api 'com.google.truth:truth:' + truthVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion 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') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } From d587def451aab4d6fed6b09797c615e82e8eaf8e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Nov 2019 17:41:05 +0000 Subject: [PATCH 684/807] Fix Javadoc broken due to lack of import PiperOrigin-RevId: 278399475 --- .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 1 + 1 file changed, 1 insertion(+) 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 041435482c..3e8cdd1ca4 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.trackselection; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; From 9842ea7f22ec6cb10e43c7a11571f903a16cb3d5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Nov 2019 17:47:22 +0000 Subject: [PATCH 685/807] Move classes that don't belong in testutils out of testutils PiperOrigin-RevId: 278401000 --- .../extractor/ogg/DefaultOggSeekerTest.java | 1 - .../extractor/ogg/OggExtractorTest.java | 1 - .../exoplayer2/extractor/ogg/OggPacketTest.java | 1 - .../extractor/ogg/OggPageHeaderTest.java | 1 - .../exoplayer2/extractor/ogg}/OggTestData.java | 7 +++++-- .../exoplayer2/extractor/ogg/OggTestFile.java | 1 - .../extractor/ogg/VorbisReaderTest.java | 1 - .../exoplayer2/extractor/ogg/VorbisUtilTest.java | 1 - .../playbacktests/gts/DashTestData.java | 6 ++---- .../playbacktests/gts/DashTestRunner.java | 4 +--- .../gts}/DebugRenderersFactory.java | 4 ++-- .../playbacktests/gts/EnumerateDecodersTest.java | 1 - .../playbacktests/gts}/LogcatMetricsLogger.java | 13 +++---------- .../playbacktests/gts}/MetricsLogger.java | 16 +++------------- 14 files changed, 16 insertions(+), 42 deletions(-) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg}/OggTestData.java (99%) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts}/DebugRenderersFactory.java (98%) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts}/LogcatMetricsLogger.java (78%) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts}/MetricsLogger.java (85%) 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 8ba0be26a0..e97fa878f7 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 @@ -22,7 +22,6 @@ 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; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java index b09f7f204f..54a14abeef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java @@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import org.junit.Test; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java index 70f64d3dbe..18a03ddc29 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java @@ -20,7 +20,6 @@ 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.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.IOException; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java index 3c8911adec..4d9e08a12d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; -import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import org.junit.Test; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestData.java similarity index 99% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestData.java index 8dd0cd16b1..c963a8f658 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestData.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.extractor.ogg; + +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; /** Provides ogg/vorbis test data in bytes for unit tests. */ -public final class OggTestData { +/* package */ final class OggTestData { public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { return new FakeExtractorInput.Builder() 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 38e4332b16..a334c5128e 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.ogg; import static org.junit.Assert.fail; -import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.util.ArrayList; import java.util.Random; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java index ab521fc99e..587d8a75a7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; -import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import org.junit.Test; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java index dc3b1510ef..6dfddb37dd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.util.ParsableByteArray; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java index 45cdf34b6c..2033ef3096 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -17,10 +17,8 @@ package com.google.android.exoplayer2.playbacktests.gts; import com.google.android.exoplayer2.util.Util; -/** - * Test data for DASH tests. - */ -public final class DashTestData { +/** Test data for DASH tests. */ +/* package */ final class DashTestData { private static final String BASE_URL = "https://storage.googleapis.com/exoplayer-test-media-1/gen-4/"; 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 052cd7d0a2..8323d66614 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 @@ -40,12 +40,10 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule; -import com.google.android.exoplayer2.testutil.DebugRenderersFactory; import com.google.android.exoplayer2.testutil.DecoderCountersUtil; import com.google.android.exoplayer2.testutil.ExoHostedTest; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; -import com.google.android.exoplayer2.testutil.MetricsLogger; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.RandomTrackSelection; @@ -62,7 +60,7 @@ import java.util.Arrays; import java.util.List; /** {@link DashHostedTest} builder. */ -public final class DashTestRunner { +/* package */ final class DashTestRunner { static final int VIDEO_RENDERER_INDEX = 0; static final int AUDIO_RENDERER_INDEX = 1; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java similarity index 98% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index d6b72048a1..affd762f61 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.playbacktests.gts; import android.annotation.TargetApi; import android.content.Context; @@ -41,7 +41,7 @@ import java.util.ArrayList; * video buffer timestamp assertions, and modifies the default value for {@link * #setAllowedVideoJoiningTimeMs(long)} to be {@code 0}. */ -public class DebugRenderersFactory extends DefaultRenderersFactory { +/* package */ final class DebugRenderersFactory extends DefaultRenderersFactory { public DebugRenderersFactory(Context context) { super(context); diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java index 73d72446f9..f7b376d7ad 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.java @@ -25,7 +25,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.testutil.MetricsLogger; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/LogcatMetricsLogger.java similarity index 78% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/LogcatMetricsLogger.java index f3432749d0..94a07b9aaf 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/LogcatMetricsLogger.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.playbacktests.gts; import com.google.android.exoplayer2.util.Log; -/** - * Implementation of {@link MetricsLogger} that prints the metrics to logcat. - */ -public final class LogcatMetricsLogger implements MetricsLogger { +/** Implementation of {@link MetricsLogger} that prints the metrics to logcat. */ +/* package */ final class LogcatMetricsLogger implements MetricsLogger { private final String tag; @@ -33,11 +31,6 @@ public final class LogcatMetricsLogger implements MetricsLogger { Log.d(tag, key + ": " + value); } - @Override - public void logMetric(String key, double value) { - Log.d(tag, key + ": " + value); - } - @Override public void logMetric(String key, String value) { Log.d(tag, key + ": " + value); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/MetricsLogger.java similarity index 85% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/MetricsLogger.java index 9edccadcab..1aeb73a29d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/MetricsLogger.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.playbacktests.gts; -/** - * Metric Logging interface for ExoPlayer playback tests. - */ -public interface MetricsLogger { +/** Metric logging interface for playback tests. */ +/* package */ interface MetricsLogger { String KEY_FRAMES_DROPPED_COUNT = "frames_dropped_count"; String KEY_FRAMES_RENDERED_COUNT = "frames_rendered_count"; @@ -35,14 +33,6 @@ public interface MetricsLogger { */ void logMetric(String key, int value); - /** - * Logs a double metric provided from a test. - * - * @param key The key of the metric to be logged. - * @param value The value of the metric to be logged. - */ - void logMetric(String key, double value); - /** * Logs a string metric provided from a test. * From c8170e18d026c8f4f3fcd25b3a9cd9123fc5f723 Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 5 Nov 2019 11:09:20 +0000 Subject: [PATCH 686/807] Update AndroidX Test versions to latest Split the version of the sublibraries because their latest version number is different. See https://developer.android.com/jetpack/androidx/releases/test#1.2.0. PiperOrigin-RevId: 278585090 --- constants.gradle | 5 ++++- extensions/flac/build.gradle | 4 +++- extensions/opus/build.gradle | 4 ++-- extensions/vp9/build.gradle | 4 ++-- library/core/build.gradle | 8 ++++---- playbacktests/build.gradle | 4 ++-- testutils/build.gradle | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/constants.gradle b/constants.gradle index 3813ba3533..ad37d86cb2 100644 --- a/constants.gradle +++ b/constants.gradle @@ -30,7 +30,10 @@ project.ext { androidxAppCompatVersion = '1.1.0' androidxCollectionVersion = '1.1.0' androidxMediaVersion = '1.0.1' - androidxTestVersion = '1.1.0' + androidxTestCoreVersion = '1.2.0' + androidxTestJUnitVersion = '1.1.1' + androidxTestRunnerVersion = '1.2.0' + androidxTestRulesVersion = '1.2.0' truthVersion = '0.44' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 5d68711aa7..4a326ac646 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -42,7 +42,9 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') - androidTestImplementation 'androidx.test:runner:' + androidxTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + testImplementation 'androidx.test:core:' + androidxTestCoreVersion + testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 2759299d63..28cf8f138f 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -42,8 +42,8 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion - androidTestImplementation 'androidx.test:runner:' + androidxTestVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion } ext { diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index e40d6e02d7..80239beb22 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -42,8 +42,8 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion - androidTestImplementation 'androidx.test:runner:' + androidxTestVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion } diff --git a/library/core/build.gradle b/library/core/build.gradle index 33798c0c30..32beddfd89 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -62,14 +62,14 @@ dependencies { compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion - androidTestImplementation 'androidx.test:runner:' + androidxTestVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidxTestVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion - testImplementation 'androidx.test:core:' + androidxTestVersion - testImplementation 'androidx.test.ext:junit:' + androidxTestVersion + testImplementation 'androidx.test:core:' + androidxTestCoreVersion + testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index d77944cf23..0e93b97f5e 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -32,8 +32,8 @@ android { } dependencies { - androidTestImplementation 'androidx.test:rules:' + androidxTestVersion - androidTestImplementation 'androidx.test:runner:' + androidxTestVersion + androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'androidx.annotation:annotation:' + androidxAnnotationVersion androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') diff --git a/testutils/build.gradle b/testutils/build.gradle index 0d6439cf6c..204e089bd0 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -39,8 +39,8 @@ android { dependencies { api 'org.mockito:mockito-core:' + mockitoVersion - api 'androidx.test:core:' + androidxTestVersion - api 'androidx.test.ext:junit:' + androidxTestVersion + api 'androidx.test:core:' + androidxTestCoreVersion + api 'androidx.test.ext:junit:' + androidxTestJUnitVersion api 'com.google.truth:truth:' + truthVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') From 7cc3943b4fba25ebf265a63b5f0dfb5ae0e33422 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 5 Nov 2019 15:35:02 +0000 Subject: [PATCH 687/807] Experimental API to skip MediaCodec.stop() Add experimental API on MediaCodecRenderer to skip calling MediaCodec.stop() before the call to MediaCodec.release(). PiperOrigin-RevId: 278621032 --- .../mediacodec/MediaCodecRenderer.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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 36ece26254..8f8d600f93 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 @@ -372,6 +372,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean waitingForKeys; private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; + private boolean skipMediaCodecStopOnRelease; protected DecoderCounters decoderCounters; @@ -433,6 +434,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { this.renderTimeLimitMs = renderTimeLimitMs; } + /** + * Skip calling {@link MediaCodec#stop()} when the underlying MediaCodec is going to be released. + * + *

        By default, when the MediaCodecRenderer is releasing the underlying {@link MediaCodec}, it + * first calls {@link MediaCodec#stop()} and then calls {@link MediaCodec#release()}. If this + * feature is enabled, the MediaCodecRenderer will skip the call to {@link MediaCodec#stop()}. + * + *

        This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param enabled enable or disable the feature. + */ + public void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) { + skipMediaCodecStopOnRelease = enabled; + } + @Override public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; @@ -636,7 +653,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codec != null) { decoderCounters.decoderReleaseCount++; try { - codec.stop(); + if (!skipMediaCodecStopOnRelease) { + codec.stop(); + } } finally { codec.release(); } From 02a83a537732771dba22d3ac1ce9ace934f6f803 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Nov 2019 17:08:02 +0000 Subject: [PATCH 688/807] Delete unused theme PiperOrigin-RevId: 278638978 --- demos/surface/src/main/res/values/styles.xml | 23 -------------------- 1 file changed, 23 deletions(-) delete mode 100644 demos/surface/src/main/res/values/styles.xml diff --git a/demos/surface/src/main/res/values/styles.xml b/demos/surface/src/main/res/values/styles.xml deleted file mode 100644 index aaa1e2ef83..0000000000 --- a/demos/surface/src/main/res/values/styles.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - From efc7f55616f9cf909a3243a7a9b1d9b87a72217e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 5 Nov 2019 17:12:17 +0000 Subject: [PATCH 689/807] Make DefaultDrmSession package private PiperOrigin-RevId: 278639779 --- .../exoplayer2/drm/DefaultDrmSession.java | 2 +- .../drm/DefaultDrmSessionManager.java | 70 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) 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 14e813ceb8..d962efd96b 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 @@ -48,7 +48,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -public class DefaultDrmSession implements DrmSession { +/* package */ class DefaultDrmSession implements DrmSession { /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */ public static final class UnexpectedDrmSessionException extends IOException { 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 f9606d591e..753b8a7d3a 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 @@ -23,7 +23,6 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; @@ -45,8 +44,7 @@ import java.util.UUID; /** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager - implements DrmSessionManager, ProvisioningManager { +public class DefaultDrmSessionManager implements DrmSessionManager { /** * Builder for {@link DefaultDrmSessionManager} instances. @@ -230,6 +228,7 @@ public class DefaultDrmSessionManager private final boolean multiSession; private final boolean preferSecureDecoders; @Flags private final int flags; + private final ProvisioningManagerImpl provisioningManagerImpl; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final List> sessions; @@ -348,6 +347,7 @@ public class DefaultDrmSessionManager this.preferSecureDecoders = preferSecureDecoders; this.flags = flags; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + provisioningManagerImpl = new ProvisioningManagerImpl(); mode = MODE_PLAYBACK; sessions = new ArrayList<>(); provisioningSessions = new ArrayList<>(); @@ -536,37 +536,6 @@ public class DefaultDrmSessionManager : null; } - // ProvisioningManager implementation. - - @Override - public void provisionRequired(DefaultDrmSession session) { - if (provisioningSessions.contains(session)) { - // The session has already requested provisioning. - return; - } - provisioningSessions.add(session); - if (provisioningSessions.size() == 1) { - // This is the first session requesting provisioning, so have it perform the operation. - session.provision(); - } - } - - @Override - public void onProvisionCompleted() { - for (DefaultDrmSession session : provisioningSessions) { - session.onProvisionCompleted(); - } - provisioningSessions.clear(); - } - - @Override - public void onProvisionError(Exception error) { - for (DefaultDrmSession session : provisioningSessions) { - session.onProvisionError(error); - } - provisioningSessions.clear(); - } - // Internal methods. private void assertExpectedPlaybackLooper(Looper playbackLooper) { @@ -586,7 +555,7 @@ public class DefaultDrmSessionManager return new DefaultDrmSession<>( uuid, exoMediaDrm, - /* provisioningManager= */ this, + /* provisioningManager= */ provisioningManagerImpl, /* releaseCallback= */ this::onSessionReleased, schemeDatas, mode, @@ -664,6 +633,37 @@ public class DefaultDrmSessionManager } } + private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { + @Override + public void provisionRequired(DefaultDrmSession session) { + if (provisioningSessions.contains(session)) { + // The session has already requested provisioning. + return; + } + provisioningSessions.add(session); + if (provisioningSessions.size() == 1) { + // This is the first session requesting provisioning, so have it perform the operation. + session.provision(); + } + } + + @Override + public void onProvisionCompleted() { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionCompleted(); + } + provisioningSessions.clear(); + } + + @Override + public void onProvisionError(Exception error) { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionError(error); + } + provisioningSessions.clear(); + } + } + private class MediaDrmEventListener implements OnEventListener { @Override From f51f7bd405d30809d61654b738ac902de864c796 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Nov 2019 17:28:21 +0000 Subject: [PATCH 690/807] Fix SurfaceControl demo app layout The fixes sizes could end up being wider than the screen (e.g on Pixel 3a) PiperOrigin-RevId: 278642828 --- .../exoplayer2/surfacedemo/MainActivity.java | 8 ++--- .../src/main/res/layout/main_activity.xml | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java index ca011434ac..99bc0d7abc 100644 --- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java +++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java @@ -124,10 +124,10 @@ public final class MainActivity extends Activity { } gridLayout.addView(view); GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(); - layoutParams.width = 400; - layoutParams.height = 400; - layoutParams.columnSpec = GridLayout.spec(i % 3); - layoutParams.rowSpec = GridLayout.spec(i / 3); + layoutParams.width = 0; + layoutParams.height = 0; + layoutParams.columnSpec = GridLayout.spec(i % 3, 1f); + layoutParams.rowSpec = GridLayout.spec(i / 3, 1f); layoutParams.bottomMargin = 10; layoutParams.leftMargin = 10; layoutParams.topMargin = 10; diff --git a/demos/surface/src/main/res/layout/main_activity.xml b/demos/surface/src/main/res/layout/main_activity.xml index d4b7fc77cd..829602275d 100644 --- a/demos/surface/src/main/res/layout/main_activity.xml +++ b/demos/surface/src/main/res/layout/main_activity.xml @@ -20,24 +20,32 @@ android:layout_height="match_parent" android:keepScreenOn="true"> - + android:orientation="vertical"> + + + + + + - - + android:visibility="gone"/> From 87003b30fce2860dc734025b00fcf06de8da4ded Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Nov 2019 18:34:29 +0000 Subject: [PATCH 691/807] Bump version to 2.10.7 PiperOrigin-RevId: 278658259 --- RELEASENOTES.md | 20 +++++++++++-------- constants.gradle | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3cbb45c028..5d1d054fed 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,8 +2,6 @@ ### dev-v2 (not yet released) ### -* MediaSession extension: Update shuffle and repeat modes when playback state - is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). * AV1 extension: Uses libgav1 to decode AV1 videos. Android 10 includes an AV1 decoder, but the older versions of Android require this extension for playback of AV1 streams ([#3353](https://github.com/google/ExoPlayer/issues/3353)). @@ -97,16 +95,10 @@ fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. * Make show and hide player controls accessible for TalkBack in `PlayerView`. -* Add workaround to avoid truncating MP3 live streams with ICY metadata and - introductions that have a seeking header - ([#6537](https://github.com/google/ExoPlayer/issues/6537), - [#6315](https://github.com/google/ExoPlayer/issues/6315) and - [#5658](https://github.com/google/ExoPlayer/issues/5658)). * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. * Deprecate the GVR extension. * Fix the start of audio getting truncated when transitioning to a new item in a playlist of opus streams. -* Fix detection of Dolby Atmos in HLS to match the HLS authoring specification. * Fix FLAC extension build ([#6601](https://github.com/google/ExoPlayer/issues/6601). * Update the ffmpeg, flac and opus extension build instructions to use NDK r20. @@ -116,6 +108,14 @@ * Add support for subtitle files to the demo app ([#5523](https://github.com/google/ExoPlayer/issues/5523)). +### 2.10.7 (2019-11-12) ### + +* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification. +* MediaSession extension: Update shuffle and repeat modes when playback state + is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). +* Fix the start of audio getting truncated when transitioning to a new + item in a playlist of opus streams. + ### 2.10.6 (2019-10-17) ### * Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to @@ -128,6 +128,10 @@ ([#6523](https://github.com/google/ExoPlayer/issues/6523)). * HLS: Add support for ID3 in EMSG when using FMP4 streams ([spec](https://aomediacodec.github.io/av1-id3/)). +* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast + live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537), + [#6315](https://github.com/google/ExoPlayer/issues/6315) and + [#5658](https://github.com/google/ExoPlayer/issues/5658)). * Metadata: Expose the raw ICY metadata through `IcyInfo` ([#6476](https://github.com/google/ExoPlayer/issues/6476)). * UI: diff --git a/constants.gradle b/constants.gradle index ad37d86cb2..e957bf3f6a 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.6' - releaseVersionCode = 2010006 + releaseVersion = '2.10.7' + releaseVersionCode = 2010007 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 29 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 adc05eb204..79d395a858 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.6"; + public static final String VERSION = "2.10.7"; /** 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.6"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.7"; /** * 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 = 2010006; + public static final int VERSION_INT = 2010007; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 6d9c707255eb369be2b2509db910029f9034fbf7 Mon Sep 17 00:00:00 2001 From: Stanislav Ionascu Date: Thu, 14 Nov 2019 08:30:30 +0100 Subject: [PATCH 692/807] Detect Dolby Vision profile 7 In official documentation dvProfile 7 uses dvhe as the codec type. --- .../com/google/android/exoplayer2/video/DolbyVisionConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java index 3aeff9d553..3a13540e12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java @@ -36,7 +36,7 @@ public final class DolbyVisionConfig { int dvProfile = (profileData >> 1); int dvLevel = ((profileData & 0x1) << 5) | ((data.readUnsignedByte() >> 3) & 0x1F); String codecsPrefix; - if (dvProfile == 4 || dvProfile == 5) { + if (dvProfile == 4 || dvProfile == 5 || dvProfile == 7) { codecsPrefix = "dvhe"; } else if (dvProfile == 8) { codecsPrefix = "hev1"; From 0a27d7b4827b939a8e28b732e78c28b90e00f873 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Nov 2019 18:43:25 +0000 Subject: [PATCH 693/807] Don't use DRM prior to API level 18 PiperOrigin-RevId: 278660557 --- .../google/android/exoplayer2/castdemo/PlayerManager.java | 3 ++- .../android/exoplayer2/drm/OfflineLicenseHelper.java | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 894012664c..85104e0d18 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; import java.util.ArrayList; @@ -408,7 +409,7 @@ import java.util.Map; DrmSessionManager drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; - if (drmConfiguration != null) { + if (drmConfiguration != null && Util.SDK_INT >= 18) { String licenseServerUrl = drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : ""; HttpMediaDrmCallback drmCallback = 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 79dc743bc9..9ed2fe3f27 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 @@ -15,12 +15,14 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.TargetApi; import android.media.MediaDrm; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; @@ -30,9 +32,9 @@ import com.google.android.exoplayer2.util.Assertions; import java.util.HashMap; import java.util.UUID; -/** - * Helper class to download, renew and release offline licenses. - */ +/** Helper class to download, renew and release offline licenses. */ +@TargetApi(18) +@RequiresApi(18) public final class OfflineLicenseHelper { private static final DrmInitData DUMMY_DRM_INIT_DATA = new DrmInitData(); From 4570cd37c505dd5de0b57245436fee63272dba18 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Nov 2019 13:09:45 +0000 Subject: [PATCH 694/807] Testutils: Add missing Javadoc + Misc cleanup PiperOrigin-RevId: 278835106 --- .../exoplayer2/database/VersionTableTest.java | 2 +- .../extractor/ogg/OggExtractorTest.java | 18 ++--- .../offline/DownloadManagerTest.java | 2 +- .../cache/CachedContentIndexTest.java | 2 +- .../cache/CachedRegionTrackerTest.java | 2 +- .../upstream/cache/SimpleCacheSpanTest.java | 2 +- .../upstream/cache/SimpleCacheTest.java | 2 +- .../dash/offline/DownloadManagerDashTest.java | 2 +- .../dash/offline/DownloadServiceDashTest.java | 2 +- .../exoplayer2/testutil/ActionSchedule.java | 1 + .../exoplayer2/testutil/ExtractorAsserts.java | 25 ++++++- .../android/exoplayer2/testutil/TestUtil.java | 75 ++++++++++++++----- 12 files changed, 100 insertions(+), 35 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java index 08b0c52fe5..2d74175265 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java @@ -38,7 +38,7 @@ public class VersionTableTest { @Before public void setUp() { - databaseProvider = TestUtil.getTestDatabaseProvider(); + databaseProvider = TestUtil.getInMemoryDatabaseProvider(); database = databaseProvider.getWritableDatabase(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java index 54a14abeef..b80c8d3892 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.extractor.ogg; -import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; @@ -59,7 +58,7 @@ public final class OggExtractorTest { OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertThat(sniff(data)).isTrue(); + assertSniff(data, /* expectedResult= */ true); } @Test @@ -69,7 +68,7 @@ public final class OggExtractorTest { OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(5), // Laces new byte[] {0x7F, 'F', 'L', 'A', 'C'}); - assertThat(sniff(data)).isTrue(); + assertSniff(data, /* expectedResult= */ true); } @Test @@ -77,13 +76,13 @@ public final class OggExtractorTest { byte[] data = TestUtil.joinByteArrays( OggTestData.buildOggHeader(0x02, 0, 1000, 0x00), new byte[] {'O', 'p', 'u', 's'}); - assertThat(sniff(data)).isFalse(); + assertSniff(data, /* expectedResult= */ false); } @Test public void testSniffFailsInvalidOggHeader() throws Exception { byte[] data = OggTestData.buildOggHeader(0x00, 0, 1000, 0x00); - assertThat(sniff(data)).isFalse(); + assertSniff(data, /* expectedResult= */ false); } @Test @@ -93,16 +92,17 @@ public final class OggExtractorTest { OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); - assertThat(sniff(data)).isFalse(); + assertSniff(data, /* expectedResult= */ false); } @Test public void testSniffFailsEOF() throws Exception { byte[] data = OggTestData.buildOggHeader(0x02, 0, 1000, 0x00); - assertThat(sniff(data)).isFalse(); + assertSniff(data, /* expectedResult= */ false); } - private boolean sniff(byte[] data) throws InterruptedException, IOException { + private void assertSniff(byte[] data, boolean expectedResult) + throws InterruptedException, IOException { FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(data) @@ -110,6 +110,6 @@ public final class OggExtractorTest { .setSimulateUnknownLength(true) .setSimulatePartialReads(true) .build(); - return TestUtil.sniffTestData(OGG_EXTRACTOR_FACTORY.create(), input); + ExtractorAsserts.assertSniff(OGG_EXTRACTOR_FACTORY.create(), input, expectedResult); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 3a3067853e..452f20e957 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -81,7 +81,7 @@ public class DownloadManagerTest { uri2 = Uri.parse("http://abc.com/media2"); uri3 = Uri.parse("http://abc.com/media3"); dummyMainThread = new DummyMainThread(); - downloadIndex = new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider()); + downloadIndex = new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()); downloaderFactory = new FakeDownloaderFactory(); setUpDownloadManager(100); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index e28277d945..fda57cbce4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -334,7 +334,7 @@ public class CachedContentIndexTest { } private CachedContentIndex newInstance() { - return new CachedContentIndex(TestUtil.getTestDatabaseProvider()); + return new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider()); } private CachedContentIndex newLegacyInstance() { 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 73780f56f3..a1c4d2b59d 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 @@ -66,7 +66,7 @@ public final class CachedRegionTrackerTest { tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); - index = new CachedContentIndex(TestUtil.getTestDatabaseProvider()); + index = new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider()); } @After diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 39be9fbcd8..5908c7db20 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -51,7 +51,7 @@ public class SimpleCacheSpanTest { public void setUp() throws Exception { cacheDir = Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); - index = new CachedContentIndex(TestUtil.getTestDatabaseProvider()); + index = new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider()); } @After 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 fc229d9dc6..a8dbfe3b42 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 @@ -322,7 +322,7 @@ public class SimpleCacheTest { @Test public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception { CachedContentIndex contentIndex = - Mockito.spy(new CachedContentIndex(TestUtil.getTestDatabaseProvider())); + Mockito.spy(new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider())); SimpleCache simpleCache = new SimpleCache( cacheDir, new LeastRecentlyUsedCacheEvictor(20), contentIndex, /* fileIndex= */ null); 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 5eec84408b..264b5d39e1 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 @@ -97,7 +97,7 @@ public class DownloadManagerDashTest { fakeStreamKey1 = new StreamKey(0, 0, 0); fakeStreamKey2 = new StreamKey(0, 1, 0); - downloadIndex = new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider()); + downloadIndex = new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()); createDownloadManager(); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 1527c5a544..fd295ea18d 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -112,7 +112,7 @@ public class DownloadServiceDashTest { dummyMainThread.runTestOnMainThread( () -> { DefaultDownloadIndex downloadIndex = - new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider()); + new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()); final DownloadManager dashDownloadManager = new DownloadManager( ApplicationProvider.getApplicationContext(), 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 cf363f6266..b1ab3f94bb 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 @@ -467,6 +467,7 @@ public final class ActionSchedule { return apply(new ThrowPlaybackException(tag, exception)); } + /** Builds the schedule. */ public ActionSchedule build() { CallbackAction callbackAction = new CallbackAction(tag); apply(callbackAction); 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 a933121bc5..1ca4f1fb18 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 @@ -45,6 +45,29 @@ public final class ExtractorAsserts { private static final String DUMP_EXTENSION = ".dump"; private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; + /** + * Asserts that {@link Extractor#sniff(ExtractorInput)} returns the {@code expectedResult} for a + * given {@code input}, retrying repeatedly when {@link SimulatedIOException} is thrown. + * + * @param extractor The extractor to test. + * @param input The extractor input. + * @param expectedResult The expected return value. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertSniff( + Extractor extractor, FakeExtractorInput input, boolean expectedResult) + throws IOException, InterruptedException { + while (true) { + try { + assertThat(extractor.sniff(input)).isEqualTo(expectedResult); + return; + } catch (SimulatedIOException e) { + // Ignore. + } + } + } + /** * Asserts that an extractor behaves correctly given valid input data. Can only be used from * Robolectric tests. @@ -164,7 +187,7 @@ public final class ExtractorAsserts { .setSimulatePartialReads(simulatePartialReads).build(); if (sniffFirst) { - assertThat(TestUtil.sniffTestData(extractor, input)).isTrue(); + assertSniff(extractor, input, /* expectedResult= */ true); input.resetPeekPosition(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index facfa0d7e4..66ea480cc3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -50,17 +49,14 @@ public class TestUtil { private TestUtil() {} - public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input) - throws IOException, InterruptedException { - while (true) { - try { - return extractor.sniff(input); - } catch (SimulatedIOException e) { - // Ignore. - } - } - } - + /** + * Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)} + * until {@link C#RESULT_END_OF_INPUT} is returned. + * + * @param dataSource The source from which to read. + * @return The concatenation of all read data. + * @throws IOException If an error occurs reading from the source. + */ public static byte[] readToEnd(DataSource dataSource) throws IOException { byte[] data = new byte[1024]; int position = 0; @@ -77,6 +73,14 @@ public class TestUtil { return Arrays.copyOf(data, position); } + /** + * Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)} + * until exactly {@code length} bytes have been read. + * + * @param dataSource The source from which to read. + * @return The read data. + * @throws IOException If an error occurs reading from the source. + */ public static byte[] readExactly(DataSource dataSource, int length) throws IOException { byte[] data = new byte[length]; int position = 0; @@ -91,22 +95,49 @@ public class TestUtil { return data; } + /** + * Equivalent to {@code buildTestData(length, length)}. + * + * @param length The length of the array. + * @return The generated array. + */ public static byte[] buildTestData(int length) { return buildTestData(length, length); } + /** + * Generates an array of random bytes with the specified length. + * + * @param length The length of the array. + * @param seed A seed for an internally created {@link Random source of randomness}. + * @return The generated array. + */ public static byte[] buildTestData(int length, int seed) { return buildTestData(length, new Random(seed)); } + /** + * Generates an array of random bytes with the specified length. + * + * @param length The length of the array. + * @param random A source of randomness. + * @return The generated array. + */ public static byte[] buildTestData(int length, Random random) { byte[] source = new byte[length]; random.nextBytes(source); return source; } - public static String buildTestString(int maxLength, Random random) { - int length = random.nextInt(maxLength); + /** + * Generates a random string with the specified maximum length. + * + * @param maximumLength The maximum length of the string. + * @param random A source of randomness. + * @return The generated string. + */ + public static String buildTestString(int maximumLength, Random random) { + int length = random.nextInt(maximumLength); StringBuilder builder = new StringBuilder(length); for (int i = 0; i < length; i++) { builder.append((char) random.nextInt()); @@ -129,6 +160,12 @@ public class TestUtil { return byteArray; } + /** + * Concatenates the provided byte arrays. + * + * @param byteArrays The byte arrays to concatenate. + * @return The concatenated result. + */ public static byte[] joinByteArrays(byte[]... byteArrays) { int length = 0; for (byte[] byteArray : byteArrays) { @@ -143,24 +180,28 @@ public class TestUtil { return joined; } + /** Returns the bytes of an asset file. */ public static byte[] getByteArray(Context context, String fileName) throws IOException { return Util.toByteArray(getInputStream(context, fileName)); } + /** Returns an {@link InputStream} for reading from an asset file. */ public static InputStream getInputStream(Context context, String fileName) throws IOException { return context.getResources().getAssets().open(fileName); } + /** Returns a {@link String} read from an asset file. */ public static String getString(Context context, String fileName) throws IOException { return Util.fromUtf8Bytes(getByteArray(context, fileName)); } - public static Bitmap readBitmapFromFile(Context context, String fileName) throws IOException { + /** Returns a {@link Bitmap} read from an asset file. */ + public static Bitmap getBitmap(Context context, String fileName) throws IOException { return BitmapFactory.decodeStream(getInputStream(context, fileName)); } - public static DatabaseProvider getTestDatabaseProvider() { - // Provides an in-memory database. + /** Returns a {@link DatabaseProvider} that provides an in-memory database. */ + public static DatabaseProvider getInMemoryDatabaseProvider() { return new DefaultDatabaseProvider( new SQLiteOpenHelper( /* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) { From cd2c1f2f24c3a17ffa59f0c5ba9d17d55f141793 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 6 Nov 2019 16:38:26 +0000 Subject: [PATCH 695/807] Playlist API: Add setMediaItem() and prepare() PiperOrigin-RevId: 278867153 --- .../exoplayer2/demo/PlayerActivity.java | 3 +- .../google/android/exoplayer2/ExoPlayer.java | 55 +++++-- .../android/exoplayer2/ExoPlayerImpl.java | 71 +++++--- .../android/exoplayer2/SimpleExoPlayer.java | 53 ++++-- .../android/exoplayer2/ExoPlayerTest.java | 154 ++++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 3 +- .../exoplayer2/testutil/StubExoPlayer.java | 15 ++ 7 files changed, 305 insertions(+), 49 deletions(-) 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 2f8d0045d3..2de117e9d7 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 @@ -394,7 +394,8 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.prepare(mediaSource, !haveStartPosition, false); + player.setMediaItem(mediaSource); + player.prepare(); updateButtonVisibility(); } 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 7c8a454191..99089a2afc 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 @@ -331,26 +331,51 @@ public interface ExoPlayer extends Player { */ void retry(); - /** - * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code - * prepare(mediaSource, true, true)}. - */ - void prepare(MediaSource mediaSource); + /** Prepares the player. */ + void prepare(); /** - * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback - * position the default position in the first {@link Timeline.Window}. - * - * @param mediaSource The {@link MediaSource} to play. - * @param resetPosition Whether the playback position should be reset to the default position in - * the first {@link Timeline.Window}. If false, playback will start from the position defined - * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. - * @param resetState Whether the timeline, manifest, tracks and track selections should be reset. - * Should be true unless the player is being prepared to play the same media as it was playing - * previously (e.g. if playback failed and is being retried). + * @deprecated Use {@code setMediaItem(mediaSource, C.TIME_UNSET)} and {@link #prepare()} instead. */ + @Deprecated + void prepare(MediaSource mediaSource); + + /** @deprecated Use {@link #setMediaItem(MediaSource, long)} and {@link #prepare()} instead. */ + @Deprecated void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); + /** + * Sets the specified {@link MediaSource}. + * + *

        Note: This is an intermediate implementation towards a larger change. Until then {@link + * #prepare()} has to be called immediately after calling this method. + * + * @param mediaItem The new {@link MediaSource}. + */ + void setMediaItem(MediaSource mediaItem); + + /** + * Sets the specified {@link MediaSource}. + * + *

        Note: This is an intermediate implementation towards a larger change. Until then {@link + * #prepare()} has to be called immediately after calling this method. + * + *

        This intermediate implementation calls {@code stop(true)} before seeking to avoid seeking in + * a media item that has been set previously. It is equivalent with calling + * + *

        
        +   *   if (!getCurrentTimeline().isEmpty()) {
        +   *     player.stop(true);
        +   *   }
        +   *   player.seekTo(0, startPositionMs);
        +   *   player.setMediaItem(mediaItem);
        +   * 
        + * + * @param mediaItem The new {@link MediaSource}. + * @param startPositionMs The position in milliseconds to start playback from. + */ + void setMediaItem(MediaSource mediaItem, long startPositionMs); + /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * will be delivered immediately without blocking on the playback thread. The default {@link 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 dd8fbee53c..97658d2906 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 @@ -62,7 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; - private MediaSource mediaSource; + @Nullable private MediaSource mediaSource; private boolean playWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; @@ -219,34 +219,38 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override + @Deprecated public void prepare(MediaSource mediaSource) { - prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + setMediaItem(mediaSource); + prepareInternal(/* resetPosition= */ true, /* resetState= */ true); } @Override + @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - this.mediaSource = mediaSource; - PlaybackInfo playbackInfo = - getResetPlaybackInfo( - resetPosition, - resetState, - /* resetError= */ true, - /* playbackState= */ Player.STATE_BUFFERING); - // Trigger internal prepare first before updating the playback info and notifying external - // listeners to ensure that new operations issued in the listener notifications reach the - // player after this prepare. The internal player can't change the playback info immediately - // because it uses a callback. - hasPendingPrepare = true; - pendingOperationAcks++; - internalPlayer.prepare(mediaSource, resetPosition, resetState); - updatePlaybackInfo( - playbackInfo, - /* positionDiscontinuity= */ false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + setMediaItem(mediaSource); + prepareInternal(resetPosition, resetState); } + @Override + public void prepare() { + Assertions.checkNotNull(mediaSource); + prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + } + + @Override + public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + if (!getCurrentTimeline().isEmpty()) { + stop(/* reset= */ true); + } + seekTo(/* windowIndex= */ 0, startPositionMs); + setMediaItem(mediaItem); + } + + @Override + public void setMediaItem(MediaSource mediaItem) { + mediaSource = mediaItem; + } @Override public void setPlayWhenReady(boolean playWhenReady) { @@ -606,6 +610,29 @@ import java.util.concurrent.CopyOnWriteArrayList; } } + /* package */ void prepareInternal(boolean resetPosition, boolean resetState) { + Assertions.checkNotNull(mediaSource); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, + resetState, + /* resetError= */ true, + /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; + internalPlayer.prepare(mediaSource, resetPosition, resetState); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); + } + private void handlePlaybackParameters( PlaybackParameters playbackParameters, boolean operationAck) { if (operationAck) { 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 78d6f1d9d8..c0a45249e6 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 @@ -1140,6 +1140,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @SuppressWarnings("deprecation") public void retry() { verifyApplicationThread(); if (mediaSource != null @@ -1149,23 +1150,38 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @Deprecated + @SuppressWarnings("deprecation") public void prepare(MediaSource mediaSource) { prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override + @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { verifyApplicationThread(); - if (this.mediaSource != null) { - this.mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - } - this.mediaSource = mediaSource; - mediaSource.addEventListener(eventHandler, analyticsCollector); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepare(mediaSource, resetPosition, resetState); + setMediaItem(mediaSource); + prepareInternal(resetPosition, resetState); + } + + @Override + public void prepare() { + verifyApplicationThread(); + prepareInternal(/* resetPosition= */ false, /* resetState= */ true); + } + + @Override + public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + verifyApplicationThread(); + setMediaItemInternal(mediaItem); + player.setMediaItem(mediaItem, startPositionMs); + } + + @Override + public void setMediaItem(MediaSource mediaItem) { + verifyApplicationThread(); + setMediaItemInternal(mediaItem); + player.setMediaItem(mediaItem); } @Override @@ -1410,6 +1426,23 @@ public class SimpleExoPlayer extends BasePlayer // Internal methods. + private void prepareInternal(boolean resetPosition, boolean resetState) { + Assertions.checkNotNull(mediaSource); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + player.prepareInternal(resetPosition, resetState); + } + + private void setMediaItemInternal(MediaSource mediaItem) { + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + mediaSource = mediaItem; + mediaSource.addEventListener(eventHandler, analyticsCollector); + } + private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { 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 79103bf0be..7146e2e405 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; @@ -33,7 +34,10 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.ClippingMediaSource; +import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.LoopingMediaSource; +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.MediaSourceEventListener.EventDispatcher; @@ -1582,6 +1586,7 @@ public final class ExoPlayerTest { AtomicInteger counter = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessagesFromStartPositionOnlyOnce") + .waitForTimelineChanged() .pause() .sendMessage( (messageType, payload) -> { @@ -2860,6 +2865,155 @@ public final class ExoPlayerTest { assertThat(seenPlaybackSuppression.get()).isFalse(); } + @Test + public void testDelegatingMediaSourceApproach() throws Exception { + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); + final ConcatenatingMediaSource underlyingSource = new ConcatenatingMediaSource(); + CompositeMediaSource delegatingMediaSource = + new CompositeMediaSource() { + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + underlyingSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + underlyingSource.addMediaSource( + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); + prepareChildSource(null, underlyingSource); + } + + @Override + public MediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + return underlyingSource.createPeriod(id, allocator, startPositionUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + underlyingSource.releasePeriod(mediaPeriod); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline timeline) { + refreshSourceInfo(timeline); + } + }; + int[] currentWindowIndices = new int[1]; + long[] currentPlaybackPositions = new long[1]; + long[] windowCounts = new long[1]; + int seekToWindowIndex = 1; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testDelegatingMediaSourceApproach") + .seek(/* windowIndex= */ 1, /* positionMs= */ 5000) + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPlaybackPositions[0] = player.getCurrentPosition(); + windowCounts[0] = player.getCurrentTimeline().getWindowCount(); + } + }) + .build(); + ExoPlayerTestRunner exoPlayerTestRunner = + new Builder() + .setMediaSource(delegatingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + exoPlayerTestRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + assertArrayEquals(new long[] {2}, windowCounts); + assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); + assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); + } + + @Test + public void testSeekTo_windowIndexIsReset_deprecated() throws Exception { + FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); + LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); + final int[] windowIndex = {C.INDEX_UNSET}; + final long[] positionMs = {C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekTo_windowIndexIsReset_deprecated") + .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .waitForSeekProcessed() + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + //noinspection deprecation + player.prepare(mediaSource); + player.seekTo(/* positionMs= */ 5000); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[0] = player.getCurrentWindowIndex(); + positionMs[0] = player.getCurrentPosition(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(loopingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(5000L); + } + + @Test + public void testSeekTo_windowIndexIsReset() throws Exception { + FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); + FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); + LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); + final int[] windowIndex = {C.INDEX_UNSET}; + final long[] positionMs = {C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekTo_windowIndexIsReset") + .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) + .waitForSeekProcessed() + .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setMediaItem(mediaSource, /* positionMs= */ 5000); + player.prepare(); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowIndex[0] = player.getCurrentWindowIndex(); + positionMs[0] = player.getCurrentPosition(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(loopingMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + + assertThat(windowIndex[0]).isEqualTo(0); + assertThat(positionMs[0]).isAtLeast(5000L); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { 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 d64a44ac04..bf3cc90a78 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 @@ -431,7 +431,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + player.setMediaItem(mediaSource); + player.prepare(); } catch (Exception e) { handleException(e); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 18eaec2cd7..47f34712b9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -96,6 +96,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void prepare() { + throw new UnsupportedOperationException(); + } + @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -106,6 +111,16 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setMediaItem(MediaSource mediaItem) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMediaItem(MediaSource mediaItem, long startPositionMs) { + throw new UnsupportedOperationException(); + } + @Override public void setPlayWhenReady(boolean playWhenReady) { throw new UnsupportedOperationException(); From 5c2806eccabcdeb8817b1ccb20bffeb087259f42 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 6 Nov 2019 17:21:31 +0000 Subject: [PATCH 696/807] Playlist API: add Playlist and PlaylistTest PiperOrigin-RevId: 278875587 --- .../AbstractConcatenatedTimeline.java | 8 +- .../google/android/exoplayer2/Playlist.java | 707 ++++++++++++++++++ .../source/ConcatenatingMediaSource.java | 1 + .../exoplayer2/source/LoopingMediaSource.java | 1 + .../android/exoplayer2/PlaylistTest.java | 510 +++++++++++++ 5 files changed, 1222 insertions(+), 5 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/{source => }/AbstractConcatenatedTimeline.java (98%) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/Playlist.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java 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/AbstractConcatenatedTimeline.java similarity index 98% rename from library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java index 29ef1faa80..73bb49ed40 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java @@ -13,16 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source; +package com.google.android.exoplayer2; 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.source.ShuffleOrder; import com.google.android.exoplayer2.util.Assertions; /** Abstract base class for the concatenation of one or more {@link Timeline}s. */ -/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { +public abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; private final ShuffleOrder shuffleOrder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java new file mode 100644 index 0000000000..351c9d5780 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java @@ -0,0 +1,707 @@ +/* + * 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; + +import android.os.Handler; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.source.MaskingMediaPeriod; +import com.google.android.exoplayer2.source.MaskingMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +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; + +/** + * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified + * during playback. It is valid for the same {@link MediaSource} instance to be present more than + * once in the playlist. + * + *

        With the exception of the constructor, all methods are called on the playback thread. + */ +/* package */ class Playlist { + + /** Listener for source events. */ + public interface PlaylistInfoRefreshListener { + + /** + * Called when the timeline of a media item has changed and a new timeline that reflects the + * current playlist state needs to be created by calling {@link #createTimeline()}. + * + *

        Called on the playback thread. + */ + void onPlaylistUpdateRequested(); + } + + private final List mediaSourceHolders; + private final Map mediaSourceByMediaPeriod; + private final Map mediaSourceByUid; + private final PlaylistInfoRefreshListener playlistInfoListener; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; + private final HashMap childSources; + private final Set enabledMediaSourceHolders; + + private ShuffleOrder shuffleOrder; + private boolean isPrepared; + + @Nullable private TransferListener mediaTransferListener; + + @SuppressWarnings("initialization") + public Playlist(PlaylistInfoRefreshListener listener) { + playlistInfoListener = listener; + shuffleOrder = new DefaultShuffleOrder(0); + mediaSourceByMediaPeriod = new IdentityHashMap<>(); + mediaSourceByUid = new HashMap<>(); + mediaSourceHolders = new ArrayList<>(); + eventDispatcher = new MediaSourceEventListener.EventDispatcher(); + childSources = new HashMap<>(); + enabledMediaSourceHolders = new HashSet<>(); + } + + /** + * Sets the media sources replacing any sources previously contained in the playlist. + * + * @param holders The list of {@link MediaSourceHolder}s to set. + * @param shuffleOrder The new shuffle order. + * @return The new {@link Timeline}. + */ + public final Timeline setMediaSources( + List holders, ShuffleOrder shuffleOrder) { + removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); + return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder); + } + + /** + * Adds multiple {@link MediaSourceHolder}s to the playlist. + * + * @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index + * must be in the range of 0 <= index <= {@link #getSize()}. + * @param holders A list of {@link MediaSourceHolder}s to be added. + * @param shuffleOrder The new shuffle order. + * @return The new {@link Timeline}. + */ + public final Timeline addMediaSources( + int index, List holders, ShuffleOrder shuffleOrder) { + if (!holders.isEmpty()) { + this.shuffleOrder = shuffleOrder; + for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) { + MediaSourceHolder holder = holders.get(insertionIndex - index); + if (insertionIndex > 0) { + MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); + holder.reset( + /* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild + + previousTimeline.getWindowCount()); + } else { + holder.reset(/* firstWindowIndexInChild= */ 0); + } + Timeline newTimeline = holder.mediaSource.getTimeline(); + correctOffsets( + /* startIndex= */ insertionIndex, + /* windowOffsetUpdate= */ newTimeline.getWindowCount()); + mediaSourceHolders.add(insertionIndex, holder); + mediaSourceByUid.put(holder.uid, holder); + if (isPrepared) { + prepareChildSource(holder); + if (mediaSourceByMediaPeriod.isEmpty()) { + enabledMediaSourceHolders.add(holder); + } else { + disableChildSource(holder); + } + } + } + } + return createTimeline(); + } + + /** + * Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index + * (included) and a final index (excluded). + * + *

        Note: when specified range is empty, no actual media source is removed and no exception is + * thrown. + * + * @param fromIndex The initial range index, pointing to the first media source that will be + * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param toIndex The final range index, pointing to the first media source that will be left + * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param shuffleOrder The new shuffle order. + * @return The new {@link Timeline}. + * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, + * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} + */ + public final Timeline removeMediaSourceRange( + int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { + Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize()); + this.shuffleOrder = shuffleOrder; + removeMediaSourcesInternal(fromIndex, toIndex); + return createTimeline(); + } + + /** + * Moves an existing media source within the playlist. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param shuffleOrder The new shuffle order. + * @return The new {@link Timeline}. + * @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0, + * {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0 + */ + public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) { + return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder); + } + + /** + * Moves a range of media sources within the playlist. + * + *

        Note: when specified range is empty or the from index equals the new from index, no actual + * media source is moved and no exception is thrown. + * + * @param fromIndex The initial range index, pointing to the first media source of the range that + * will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param toIndex The final range index, pointing to the first media source that will be left + * untouched. This index must be larger or equals than {@code fromIndex}. + * @param newFromIndex The target index of the first media source of the range that will be moved. + * @param shuffleOrder The new shuffle order. + * @return The new {@link Timeline}. + * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, + * {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code + * newFromIndex} < 0 + */ + public Timeline moveMediaSourceRange( + int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { + Assertions.checkArgument( + fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0); + this.shuffleOrder = shuffleOrder; + if (fromIndex == toIndex || fromIndex == newFromIndex) { + return createTimeline(); + } + int startIndex = Math.min(fromIndex, newFromIndex); + int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1; + int endIndex = Math.max(newEndIndex, toIndex - 1); + int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; + moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); + for (int i = startIndex; i <= endIndex; i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.firstWindowIndexInChild = windowOffset; + windowOffset += holder.mediaSource.getTimeline().getWindowCount(); + } + return createTimeline(); + } + + /** Clears the playlist. */ + public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) { + this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear(); + removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize()); + return createTimeline(); + } + + /** Whether the playlist is prepared. */ + public final boolean isPrepared() { + return isPrepared; + } + + /** Returns the number of media sources in the playlist. */ + public final int getSize() { + return mediaSourceHolders.size(); + } + + /** + * Sets the {@link AnalyticsCollector}. + * + * @param handler The handler on which to call the collector. + * @param analyticsCollector The analytics collector. + */ + public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) { + eventDispatcher.addEventListener(handler, analyticsCollector); + } + + /** + * Sets a new shuffle order to use when shuffling the child media sources. + * + * @param shuffleOrder A {@link ShuffleOrder}. + */ + public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) { + int size = getSize(); + if (shuffleOrder.getLength() != size) { + shuffleOrder = + shuffleOrder + .cloneAndClear() + .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); + } + this.shuffleOrder = shuffleOrder; + return createTimeline(); + } + + /** Prepares the playlist. */ + public final void prepare(@Nullable TransferListener mediaTransferListener) { + Assertions.checkState(!isPrepared); + this.mediaTransferListener = mediaTransferListener; + for (int i = 0; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); + prepareChildSource(mediaSourceHolder); + enabledMediaSourceHolders.add(mediaSourceHolder); + } + isPrepared = true; + } + + /** + * Returns a new {@link MediaPeriod} identified by {@code periodId}. + * + * @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. + * @return A new {@link MediaPeriod}. + */ + public MediaPeriod createPeriod( + MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) { + Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); + MediaSource.MediaPeriodId childMediaPeriodId = + id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); + MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); + enableMediaSource(holder); + holder.activeMediaPeriodIds.add(childMediaPeriodId); + MediaPeriod mediaPeriod = + holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + mediaSourceByMediaPeriod.put(mediaPeriod, holder); + disableUnusedMediaSources(); + return mediaPeriod; + } + + /** + * Releases the period. + * + * @param mediaPeriod The period to release. + */ + public final void releasePeriod(MediaPeriod mediaPeriod) { + MediaSourceHolder holder = + Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); + holder.mediaSource.releasePeriod(mediaPeriod); + holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); + if (!mediaSourceByMediaPeriod.isEmpty()) { + disableUnusedMediaSources(); + } + maybeReleaseChildSource(holder); + } + + /** Releases the playlist. */ + public final void release() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.releaseSource(childSource.caller); + childSource.mediaSource.removeEventListener(childSource.eventListener); + } + childSources.clear(); + enabledMediaSourceHolders.clear(); + isPrepared = false; + } + + /** Throws any pending error encountered while loading or refreshing. */ + public final void maybeThrowSourceInfoRefreshError() throws IOException { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + /** Creates a timeline reflecting the current state of the playlist. */ + public final Timeline createTimeline() { + if (mediaSourceHolders.isEmpty()) { + return Timeline.EMPTY; + } + int windowOffset = 0; + for (int i = 0; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); + mediaSourceHolder.firstWindowIndexInChild = windowOffset; + windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount(); + } + return new PlaylistTimeline(mediaSourceHolders, shuffleOrder); + } + + // Internal methods. + + private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { + enabledMediaSourceHolders.add(mediaSourceHolder); + @Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder); + if (enabledChild != null) { + enabledChild.mediaSource.enable(enabledChild.caller); + } + } + + private void disableUnusedMediaSources() { + Iterator iterator = enabledMediaSourceHolders.iterator(); + while (iterator.hasNext()) { + MediaSourceHolder holder = iterator.next(); + if (holder.activeMediaPeriodIds.isEmpty()) { + disableChildSource(holder); + iterator.remove(); + } + } + } + + private void disableChildSource(MediaSourceHolder holder) { + @Nullable MediaSourceAndListener disabledChild = childSources.get(holder); + if (disabledChild != null) { + disabledChild.mediaSource.disable(disabledChild.caller); + } + } + + private void removeMediaSourcesInternal(int fromIndex, int toIndex) { + for (int index = toIndex - 1; index >= fromIndex; index--) { + MediaSourceHolder holder = mediaSourceHolders.remove(index); + mediaSourceByUid.remove(holder.uid); + Timeline oldTimeline = holder.mediaSource.getTimeline(); + correctOffsets( + /* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount()); + holder.isRemoved = true; + if (isPrepared) { + maybeReleaseChildSource(holder); + } + } + } + + private void correctOffsets(int startIndex, int windowOffsetUpdate) { + for (int i = startIndex; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); + mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate; + } + } + + // Internal methods to manage child sources. + + @Nullable + private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) { + for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) { + // Ensure the reported media period id has the same window sequence number as the one created + // by this media source. Otherwise it does not belong to this child source. + if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber + == mediaPeriodId.windowSequenceNumber) { + Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); + return mediaPeriodId.copyWithPeriodUid(periodUid); + } + } + return null; + } + + private static int getWindowIndexForChildWindowIndex( + MediaSourceHolder mediaSourceHolder, int windowIndex) { + return windowIndex + mediaSourceHolder.firstWindowIndexInChild; + } + + private void prepareChildSource(MediaSourceHolder holder) { + MediaSource mediaSource = holder.mediaSource; + MediaSource.MediaSourceCaller caller = + (source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested(); + MediaSourceEventListener eventListener = new ForwardingEventListener(holder); + childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); + mediaSource.addEventListener(new Handler(), eventListener); + mediaSource.prepareSource(caller, mediaTransferListener); + } + + private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { + // Release if the source has been removed from the playlist and no periods are still active. + if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { + MediaSourceAndListener removedChild = + Assertions.checkNotNull(childSources.remove(mediaSourceHolder)); + removedChild.mediaSource.releaseSource(removedChild.caller); + removedChild.mediaSource.removeEventListener(removedChild.eventListener); + enabledMediaSourceHolders.remove(mediaSourceHolder); + } + } + + /** Return uid of media source holder from period uid of concatenated source. */ + private static Object getMediaSourceHolderUid(Object periodUid) { + return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); + } + + /** Return uid of child period from period uid of concatenated source. */ + private static Object getChildPeriodUid(Object periodUid) { + return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); + } + + private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { + return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid); + } + + /* package */ static void moveMediaSourceHolders( + List mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) { + MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex]; + for (int i = removedItems.length - 1; i >= 0; i--) { + removedItems[i] = mediaSourceHolders.remove(fromIndex + i); + } + mediaSourceHolders.addAll( + Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems)); + } + + /** Data class to hold playlist media sources together with meta data needed to process them. */ + /* package */ static final class MediaSourceHolder { + + public final MaskingMediaSource mediaSource; + public final Object uid; + public final List activeMediaPeriodIds; + + public int firstWindowIndexInChild; + public boolean isRemoved; + + public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); + this.activeMediaPeriodIds = new ArrayList<>(); + this.uid = new Object(); + } + + public void reset(int firstWindowIndexInChild) { + this.firstWindowIndexInChild = firstWindowIndexInChild; + this.isRemoved = false; + this.activeMediaPeriodIds.clear(); + } + } + + /** Timeline exposing concatenated timelines of playlist media sources. */ + /* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline { + + private final int windowCount; + private final int periodCount; + private final int[] firstPeriodInChildIndices; + private final int[] firstWindowInChildIndices; + private final Timeline[] timelines; + private final Object[] uids; + private final HashMap childIndexByUid; + + public PlaylistTimeline( + Collection mediaSourceHolders, ShuffleOrder shuffleOrder) { + super(/* isAtomic= */ false, shuffleOrder); + int childCount = mediaSourceHolders.size(); + firstPeriodInChildIndices = new int[childCount]; + firstWindowInChildIndices = new int[childCount]; + timelines = new Timeline[childCount]; + uids = new Object[childCount]; + childIndexByUid = new HashMap<>(); + int index = 0; + int windowCount = 0; + int periodCount = 0; + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + 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 + protected int getChildIndexByPeriodIndex(int periodIndex) { + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); + } + + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + Integer index = childIndexByUid.get(childUid); + return index == null ? C.INDEX_UNSET : index; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return firstPeriodInChildIndices[childIndex]; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return firstWindowInChildIndices[childIndex]; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return uids[childIndex]; + } + + @Override + public int getWindowCount() { + return windowCount; + } + + @Override + public int getPeriodCount() { + return periodCount; + } + } + + private static final class MediaSourceAndListener { + + public final MediaSource mediaSource; + public final MediaSource.MediaSourceCaller caller; + public final MediaSourceEventListener eventListener; + + public MediaSourceAndListener( + MediaSource mediaSource, + MediaSource.MediaSourceCaller caller, + MediaSourceEventListener eventListener) { + this.mediaSource = mediaSource; + this.caller = caller; + this.eventListener = eventListener; + } + } + + private final class ForwardingEventListener implements MediaSourceEventListener { + + private final Playlist.MediaSourceHolder id; + private EventDispatcher eventDispatcher; + + public ForwardingEventListener(Playlist.MediaSourceHolder id) { + eventDispatcher = Playlist.this.eventDispatcher; + this.id = id; + } + + @Override + public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.mediaPeriodCreated(); + } + } + + @Override + public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.mediaPeriodReleased(); + } + } + + @Override + public void onLoadStarted( + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadStarted(loadEventData, mediaLoadData); + } + } + + @Override + public void onLoadCompleted( + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadCompleted(loadEventData, mediaLoadData); + } + } + + @Override + public void onLoadCanceled( + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadCanceled(loadEventData, mediaLoadData); + } + } + + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled); + } + } + + @Override + public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.readingStarted(); + } + } + + @Override + public void onUpstreamDiscarded( + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.upstreamDiscarded(mediaLoadData); + } + } + + @Override + public void onDownstreamFormatChanged( + int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.downstreamFormatChanged(mediaLoadData); + } + } + + /** Updates the event dispatcher and returns whether the event should be dispatched. */ + private boolean maybeUpdateEventDispatcher( + int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { + @Nullable MediaSource.MediaPeriodId mediaPeriodId = null; + if (childMediaPeriodId != null) { + mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); + if (mediaPeriodId == null) { + // Media period not found. Ignore event. + return false; + } + } + int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); + if (eventDispatcher.windowIndex != windowIndex + || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { + eventDispatcher = + Playlist.this.eventDispatcher.withParameters( + windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L); + } + return true; + } + } +} 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 545b8f5155..c1ab78a9bc 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 @@ -19,6 +19,7 @@ import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; 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 cedc6f911d..8769a84d95 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java new file mode 100644 index 0000000000..cc551db8ac --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java @@ -0,0 +1,510 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeShuffleOrder; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link Playlist}. */ +@RunWith(AndroidJUnit4.class) +public class PlaylistTest { + + private static final int PLAYLIST_SIZE = 4; + + private Playlist playlist; + + @Before + public void setUp() { + playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class)); + } + + @Test + public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() { + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); + List fakeHolders = createFakeHolders(); + + Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); + assertNotSame(timeline, Timeline.EMPTY); + + // Remove all media sources. + timeline = + playlist.removeMediaSourceRange( + /* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder); + assertSame(timeline, Timeline.EMPTY); + + timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); + assertNotSame(timeline, Timeline.EMPTY); + // Clear. + timeline = playlist.clear(shuffleOrder); + assertSame(timeline, Timeline.EMPTY); + } + + @Test + public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() { + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + playlist.setMediaSources( + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); + // Verify prepare is called once on prepare. + verify(mockMediaSource1, times(0)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + verify(mockMediaSource2, times(0)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + + playlist.prepare(/* mediaTransferListener= */ null); + assertThat(playlist.isPrepared()).isTrue(); + // Verify prepare is called once on prepare. + verify(mockMediaSource1, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + verify(mockMediaSource2, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + + playlist.release(); + playlist.prepare(/* mediaTransferListener= */ null); + // Verify prepare is called a second time on re-prepare. + verify(mockMediaSource1, times(2)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + verify(mockMediaSource2, times(2)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + } + + @Test + public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() { + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + List mediaSources = + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); + Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder); + + assertThat(timeline.getWindowCount()).isEqualTo(2); + assertThat(playlist.getSize()).isEqualTo(2); + + // Assert holder offsets have been set properly + for (int i = 0; i < mediaSources.size(); i++) { + Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i); + assertThat(mediaSourceHolder.isRemoved).isFalse(); + assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); + } + + // Set media items again. The second holder is re-used. + List moreMediaSources = + createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); + moreMediaSources.add(mediaSources.get(1)); + timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder); + + assertThat(playlist.getSize()).isEqualTo(2); + assertThat(timeline.getWindowCount()).isEqualTo(2); + for (int i = 0; i < moreMediaSources.size(); i++) { + Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i); + assertThat(mediaSourceHolder.isRemoved).isFalse(); + assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); + } + // Expect removed holders and sources to be removed without releasing. + verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + assertThat(mediaSources.get(0).isRemoved).isTrue(); + // Expect re-used holder and source not to be removed. + verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + assertThat(mediaSources.get(1).isRemoved).isFalse(); + } + + @Test + public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() { + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + List mediaSources = + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); + + playlist.prepare(/* mediaTransferListener= */ null); + playlist.setMediaSources(mediaSources, shuffleOrder); + + // Verify sources are prepared. + verify(mockMediaSource1, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + verify(mockMediaSource2, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + + // Set media items again. The second holder is re-used. + List moreMediaSources = + createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); + moreMediaSources.add(mediaSources.get(1)); + playlist.setMediaSources(moreMediaSources, shuffleOrder); + + // Expect removed holders and sources to be removed and released. + verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + assertThat(mediaSources.get(0).isRemoved).isTrue(); + // Expect re-used holder and source not to be removed but released. + verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + assertThat(mediaSources.get(1).isRemoved).isFalse(); + verify(mockMediaSource2, times(2)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + } + + @Test + public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() { + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + List mediaSources = + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); + playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2)); + + assertThat(playlist.getSize()).isEqualTo(2); + // Verify lazy initialization does not call prepare on sources. + verify(mockMediaSource1, times(0)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + verify(mockMediaSource2, times(0)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + + for (int i = 0; i < mediaSources.size(); i++) { + assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i); + assertThat(mediaSources.get(i).isRemoved).isFalse(); + } + + // Add for more sources in between. + List moreMediaSources = createFakeHolders(); + playlist.addMediaSources( + /* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3)); + + assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0); + assertThat(moreMediaSources.get(0).firstWindowIndexInChild).isEqualTo(1); + assertThat(moreMediaSources.get(3).firstWindowIndexInChild).isEqualTo(4); + assertThat(mediaSources.get(1).firstWindowIndexInChild).isEqualTo(5); + } + + @Test + public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() { + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + playlist.prepare(/* mediaTransferListener= */ null); + playlist.addMediaSources( + /* index= */ 0, + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); + + // Verify prepare is called on sources when added. + verify(mockMediaSource1, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + verify(mockMediaSource2, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + } + + @Test + public void testMoveMediaSources() { + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); + List holders = createFakeHolders(); + playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); + + assertDefaultFirstWindowInChildIndexOrder(holders); + playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder); + assertFirstWindowInChildIndices(holders, 3, 0, 1, 2); + playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder); + assertDefaultFirstWindowInChildIndexOrder(holders); + + playlist.moveMediaSourceRange( + /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); + assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); + playlist.moveMediaSourceRange( + /* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); + assertDefaultFirstWindowInChildIndexOrder(holders); + + playlist.moveMediaSourceRange( + /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); + assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); + playlist.moveMediaSourceRange( + /* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder); + assertFirstWindowInChildIndices(holders, 0, 3, 1, 2); + playlist.moveMediaSourceRange( + /* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder); + assertDefaultFirstWindowInChildIndexOrder(holders); + + // No-ops. + playlist.moveMediaSourceRange( + /* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); + assertDefaultFirstWindowInChildIndexOrder(holders); + playlist.moveMediaSourceRange( + /* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder); + assertDefaultFirstWindowInChildIndexOrder(holders); + } + + @Test + public void testRemoveMediaSources_whenUnprepared_expectNoRelease() { + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + MediaSource mockMediaSource3 = mock(MediaSource.class); + MediaSource mockMediaSource4 = mock(MediaSource.class); + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); + + List holders = + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, + mockMediaSource1, + mockMediaSource2, + mockMediaSource3, + mockMediaSource4); + playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); + playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); + + assertThat(playlist.getSize()).isEqualTo(2); + Playlist.MediaSourceHolder removedHolder1 = holders.remove(1); + Playlist.MediaSourceHolder removedHolder2 = holders.remove(1); + + assertDefaultFirstWindowInChildIndexOrder(holders); + assertThat(removedHolder1.isRemoved).isTrue(); + assertThat(removedHolder2.isRemoved).isTrue(); + verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + verify(mockMediaSource3, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + } + + @Test + public void testRemoveMediaSources_whenPrepared_expectRelease() { + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + MediaSource mockMediaSource3 = mock(MediaSource.class); + MediaSource mockMediaSource4 = mock(MediaSource.class); + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); + + List holders = + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, + mockMediaSource1, + mockMediaSource2, + mockMediaSource3, + mockMediaSource4); + playlist.prepare(/* mediaTransferListener */ null); + playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); + playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); + + assertThat(playlist.getSize()).isEqualTo(2); + holders.remove(2); + holders.remove(1); + + assertDefaultFirstWindowInChildIndexOrder(holders); + verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + verify(mockMediaSource3, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + } + + @Test + public void testRelease_playlistUnprepared_expectSourcesNotReleased() { + MediaSource mockMediaSource = mock(MediaSource.class); + Playlist.MediaSourceHolder mediaSourceHolder = + new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); + + playlist.setMediaSources( + Collections.singletonList(mediaSourceHolder), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); + verify(mockMediaSource, times(0)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + playlist.release(); + verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + assertThat(mediaSourceHolder.isRemoved).isFalse(); + } + + @Test + public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() { + MediaSource mockMediaSource = mock(MediaSource.class); + Playlist.MediaSourceHolder mediaSourceHolder = + new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); + + playlist.prepare(/* mediaTransferListener= */ null); + playlist.setMediaSources( + Collections.singletonList(mediaSourceHolder), + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); + verify(mockMediaSource, times(1)) + .prepareSource( + any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); + playlist.release(); + verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); + assertThat(mediaSourceHolder.isRemoved).isFalse(); + } + + @Test + public void testClearPlaylist_expectSourcesReleasedAndRemoved() { + ShuffleOrder.DefaultShuffleOrder shuffleOrder = + new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); + MediaSource mockMediaSource1 = mock(MediaSource.class); + MediaSource mockMediaSource2 = mock(MediaSource.class); + List holders = + createFakeHoldersWithSources( + /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); + playlist.setMediaSources(holders, shuffleOrder); + playlist.prepare(/* mediaTransferListener= */ null); + + Timeline timeline = playlist.clear(shuffleOrder); + assertThat(timeline.isEmpty()).isTrue(); + assertThat(holders.get(0).isRemoved).isTrue(); + assertThat(holders.get(1).isRemoved).isTrue(); + verify(mockMediaSource1, times(1)).releaseSource(any()); + verify(mockMediaSource2, times(1)).releaseSource(any()); + } + + @Test + public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() { + Timeline timeline = + playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4)); + assertTimelineUsesFakeShuffleOrder(timeline); + } + + @Test + public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() { + Timeline timeline = + playlist.addMediaSources( + /* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE)); + assertTimelineUsesFakeShuffleOrder(timeline); + } + + @Test + public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() { + ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); + playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); + Timeline timeline = + playlist.moveMediaSource( + /* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE)); + assertTimelineUsesFakeShuffleOrder(timeline); + } + + @Test + public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { + ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); + playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); + Timeline timeline = + playlist.moveMediaSourceRange( + /* fromIndex= */ 0, + /* toIndex= */ 2, + /* newFromIndex= */ 2, + new FakeShuffleOrder(PLAYLIST_SIZE)); + assertTimelineUsesFakeShuffleOrder(timeline); + } + + @Test + public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { + ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); + playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); + Timeline timeline = + playlist.removeMediaSourceRange( + /* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2)); + assertTimelineUsesFakeShuffleOrder(timeline); + } + + @Test + public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() { + playlist.setMediaSources( + createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE)); + assertTimelineUsesFakeShuffleOrder( + playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE))); + } + + // Internal methods. + + private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) { + assertThat( + timeline.getNextWindowIndex( + /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) + .isEqualTo(-1); + assertThat( + timeline.getPreviousWindowIndex( + /* windowIndex= */ timeline.getWindowCount() - 1, + Player.REPEAT_MODE_OFF, + /* shuffleModeEnabled= */ true)) + .isEqualTo(-1); + } + + private static void assertDefaultFirstWindowInChildIndexOrder( + List holders) { + int[] indices = new int[holders.size()]; + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + assertFirstWindowInChildIndices(holders, indices); + } + + private static void assertFirstWindowInChildIndices( + List holders, int... firstWindowInChildIndices) { + assertThat(holders).hasSize(firstWindowInChildIndices.length); + for (int i = 0; i < holders.size(); i++) { + assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]); + } + } + + private static List createFakeHolders() { + MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1)); + List holders = new ArrayList<>(); + for (int i = 0; i < PLAYLIST_SIZE; i++) { + holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true)); + } + return holders; + } + + private static List createFakeHoldersWithSources( + boolean useLazyPreparation, MediaSource... sources) { + List holders = new ArrayList<>(); + for (MediaSource mediaSource : sources) { + holders.add( + new Playlist.MediaSourceHolder( + mediaSource, /* useLazyPreparation= */ useLazyPreparation)); + } + return holders; + } +} From 6f9baffa0cc7daf8cbfd5e1f6c55a908190d2041 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Nov 2019 09:33:59 +0000 Subject: [PATCH 697/807] Suppress warnings emitted by Checker Framework version 2.11.1 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: tap_presubmit: http://test/OCL:278683723:BASE:278762656:1573036487314:924e1b0b Some tests failed; test failures are believed to be unrelated to this CL PiperOrigin-RevId: 279034739 --- .../google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index 957c3ba209..b9ecaf174c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -49,7 +49,8 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ - @SuppressWarnings("ParameterNotNullable") + // dereference of possibly-null reference keyId + @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) public static byte[] buildPsshAtom( UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { int dataLength = data != null ? data.length : 0; From 29d110b7eb74236848d52478548d1e6537f36ae4 Mon Sep 17 00:00:00 2001 From: kimvde Date: Thu, 7 Nov 2019 14:36:53 +0000 Subject: [PATCH 698/807] Fix FLAC extension tests - Add @Test annotations. - Fix seeking to ignore potential placeholders (see https://xiph.org/flac/format.html#metadata_block_seektable). PiperOrigin-RevId: 279074092 --- .../exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java | 3 +++ .../android/exoplayer2/ext/flac/FlacExtractorSeekTest.java | 7 +++++++ .../android/exoplayer2/ext/flac/FlacExtractorTest.java | 3 +++ extensions/flac/src/main/jni/flac_parser.cc | 3 +++ 4 files changed, 16 insertions(+) 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 a3770afc78..025fdfd209 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; /** Unit test for {@link FlacBinarySearchSeeker}. */ @@ -41,6 +42,7 @@ public final class FlacBinarySearchSeekerTest { } } + @Test public void testGetSeekMap_returnsSeekMapWithCorrectDuration() throws IOException, FlacDecoderException, InterruptedException { byte[] data = @@ -63,6 +65,7 @@ public final class FlacBinarySearchSeekerTest { assertThat(seekMap.isSeekable()).isTrue(); } + @Test public void testSetSeekTargetUs_returnsSeekPending() throws IOException, FlacDecoderException, InterruptedException { byte[] data = 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 3beb4d0103..a64a52b411 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 @@ -41,6 +41,7 @@ import java.io.IOException; import java.util.List; import java.util.Random; import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; /** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */ @@ -76,6 +77,7 @@ public final class FlacExtractorSeekTest { positionHolder = new PositionHolder(); } + @Test public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -87,6 +89,7 @@ public final class FlacExtractorSeekTest { assertThat(seekMap.isSeekable()).isTrue(); } + @Test public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -103,6 +106,7 @@ public final class FlacExtractorSeekTest { trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -120,6 +124,7 @@ public final class FlacExtractorSeekTest { trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -139,6 +144,7 @@ public final class FlacExtractorSeekTest { trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -158,6 +164,7 @@ public final class FlacExtractorSeekTest { trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); 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 97f152cea4..c8033e04d3 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 @@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; /** Unit test for {@link FlacExtractor}. */ @@ -34,11 +35,13 @@ public class FlacExtractorTest { } } + @Test public void testExtractFlacSample() throws Exception { ExtractorAsserts.assertBehavior( FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext()); } + @Test public void testExtractFlacSampleWithId3Header() throws Exception { ExtractorAsserts.assertBehavior( FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext()); diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 0ddb93dbe6..b920560f3a 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -456,6 +456,9 @@ bool FLACParser::getSeekPositions(int64_t timeUs, for (unsigned i = length; i != 0; i--) { int64_t sampleNumber = points[i - 1].sample_number; + if (sampleNumber == -1) { // placeholder + continue; + } if (sampleNumber <= targetSampleNumber) { result[0] = (sampleNumber * 1000000LL) / sampleRate; result[1] = firstFrameOffset + points[i - 1].stream_offset; From 355ed11a3cd6d90eb02d9f773a546561884324b3 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Nov 2019 15:19:20 +0000 Subject: [PATCH 699/807] Suppress warnings emitted by Checker Framework version 2.11.1 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: TAP --sample ran all affected tests and none failed http://test/OCL:278915274:BASE:278884711:1573074344615:a6701677 PiperOrigin-RevId: 279080514 --- .../google/android/exoplayer2/ext/flac/FlacDecoderJni.java | 6 ++++++ .../com/google/android/exoplayer2/ui/DefaultTimeBar.java | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) 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 5e020175e7..60f1d32a79 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 @@ -51,6 +51,12 @@ import java.nio.ByteBuffer; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; + // the constructor does not initialize fields: tempBuffer + // call to flacInit() not allowed on the given receiver. + @SuppressWarnings({ + "nullness:initialization.fields.uninitialized", + "nullness:method.invocation.invalid" + }) public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { throw new FlacDecoderException("Failed to load decoder native libraries."); 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 4e7422b291..1efdeac84d 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 @@ -239,7 +239,11 @@ public class DefaultTimeBar extends View implements TimeBar { } // Suppress warnings due to usage of View methods in the constructor. - @SuppressWarnings("nullness:method.invocation.invalid") + // the constructor does not initialize fields: adGroupTimesMs, playedAdGroups + @SuppressWarnings({ + "nullness:method.invocation.invalid", + "nullness:initialization.fields.uninitialized" + }) public DefaultTimeBar( Context context, @Nullable AttributeSet attrs, From a226bd0d694d4a7e5f8eaae6022fa69e48363d24 Mon Sep 17 00:00:00 2001 From: kimvde Date: Thu, 7 Nov 2019 15:19:37 +0000 Subject: [PATCH 700/807] Fix extractor sniffing in FLAC extension Assuming that a flac stream starts with bytes ['f', 'L', 'a', 'C', 0, 0, 0, 0x22] is not always correct as it could also start with ['f', 'L', 'a', 'C', 0x80, 0, 0, 0x22] (see https://xiph.org/flac/format.html#metadata_block_streaminfo). PiperOrigin-RevId: 279080562 --- .../exoplayer2/ext/flac/FlacExtractor.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) 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 59fb7b4835..fb5d41c0de 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 @@ -72,11 +72,8 @@ public final class FlacExtractor implements Extractor { */ public static final int FLAG_DISABLE_ID3_METADATA = 1; - /** - * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the - * mandatory STREAMINFO. - */ - private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; + /** FLAC stream marker */ + private static final byte[] FLAC_STREAM_MARKER = {'f', 'L', 'a', 'C'}; private final ParsableByteArray outputBuffer; private final Id3Peeker id3Peeker; @@ -126,7 +123,7 @@ public final class FlacExtractor implements Extractor { if (input.getPosition() == 0) { id3Metadata = peekId3Data(input); } - return peekFlacSignature(input); + return peekFlacStreamMarker(input); } @Override @@ -255,15 +252,15 @@ public final class FlacExtractor implements Extractor { } /** - * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. + * Peeks from the beginning of the input to see if {@link #FLAC_STREAM_MARKER} is present. * - * @return Whether the input begins with {@link #FLAC_SIGNATURE}. + * @return Whether the input begins with {@link #FLAC_STREAM_MARKER}. */ - private static boolean peekFlacSignature(ExtractorInput input) + private static boolean peekFlacStreamMarker(ExtractorInput input) throws IOException, InterruptedException { - byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); - return Arrays.equals(header, FLAC_SIGNATURE); + byte[] header = new byte[FLAC_STREAM_MARKER.length]; + input.peekFully(header, /* offset= */ 0, FLAC_STREAM_MARKER.length); + return Arrays.equals(header, FLAC_STREAM_MARKER); } /** From d1da3d925bc4bf43c874390a98ceade045237e6c Mon Sep 17 00:00:00 2001 From: kimvde Date: Thu, 7 Nov 2019 16:05:30 +0000 Subject: [PATCH 701/807] Fix FLAC bit rate computation PiperOrigin-RevId: 279088193 --- extensions/flac/src/androidTest/assets/bear.flac.0.dump | 2 +- extensions/flac/src/androidTest/assets/bear.flac.1.dump | 2 +- extensions/flac/src/androidTest/assets/bear.flac.2.dump | 2 +- extensions/flac/src/androidTest/assets/bear.flac.3.dump | 2 +- .../flac/src/androidTest/assets/bear_with_id3.flac.0.dump | 2 +- .../flac/src/androidTest/assets/bear_with_id3.flac.1.dump | 2 +- .../flac/src/androidTest/assets/bear_with_id3.flac.2.dump | 2 +- .../flac/src/androidTest/assets/bear_with_id3.flac.3.dump | 2 +- .../com/google/android/exoplayer2/util/FlacStreamMetadata.java | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.0.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.1.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.2.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.3.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump | 2 +- .../src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump index 71359322b0..87060e8d61 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump index 820b9eed10..b12f4dbf9d 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump index c2d58347eb..613023e86c 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump index 8c1115f1ec..79f369751c 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump index d8903fcade..3a3ba57572 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump index 100fdd1eaf..a07fcaa0a2 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump index 6c3cd731b3..c4d13dd2e6 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump index decf9c6af3..2f389909e7 100644 --- a/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/raw diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 2c814294af..9c5862b483 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -109,7 +109,7 @@ public final class FlacStreamMetadata { /** Returns the bit-rate of the FLAC stream. */ public int bitRate() { - return bitsPerSample * sampleRate; + return bitsPerSample * sampleRate * channels; } /** Returns the duration of the FLAC stream in microseconds. */ diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump index 5b8d893f1a..ccd09181a8 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump index fff76c5b05..0f36b2b32e 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump index b4d3534161..b3ff58e707 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump index 27c29cba58..ea2eff8b04 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump index 5b8d893f1a..ccd09181a8 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump index 2ecdc9784c..0972d17f2f 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump index 0ed2a86b9e..e33b81c90f 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump index 229e90584e..b8b7d85393 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump index 89c6d178ff..c866017548 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index 7a4ba81f23..735d97eed1 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -5,7 +5,7 @@ seekMap: numberOfTracks = 1 track 0: format: - bitrate = 768000 + bitrate = 1536000 id = null containerMimeType = null sampleMimeType = audio/flac From 53283ecb529515185e79105d3a78009efd29aec9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 7 Nov 2019 16:27:39 +0000 Subject: [PATCH 702/807] Add CastPlayer tests for repeatMode masking PiperOrigin-RevId: 279091742 --- .../exoplayer2/ext/cast/CastPlayerTest.java | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index b2aa0b7b16..ae081b1248 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -16,15 +16,20 @@ package com.google.android.exoplayer2.ext.cast; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player; +import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastSession; import com.google.android.gms.cast.framework.SessionManager; +import com.google.android.gms.cast.framework.media.MediaQueue; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; @@ -43,6 +48,8 @@ public class CastPlayerTest { private CastPlayer castPlayer; private RemoteMediaClient.Listener remoteMediaClientListener; @Mock private RemoteMediaClient mockRemoteMediaClient; + @Mock private MediaStatus mockMediaStatus; + @Mock private MediaQueue mockMediaQueue; @Mock private CastContext mockCastContext; @Mock private SessionManager mockSessionManager; @Mock private CastSession mockCastSession; @@ -61,8 +68,12 @@ public class CastPlayerTest { when(mockCastContext.getSessionManager()).thenReturn(mockSessionManager); when(mockSessionManager.getCurrentCastSession()).thenReturn(mockCastSession); when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient); - // Make the remote media client be initially paused (most common scenario). + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockMediaQueue.getItemIds()).thenReturn(new int[0]); + // Make the remote media client present the same default values as ExoPlayer: when(mockRemoteMediaClient.isPaused()).thenReturn(true); + when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF); castPlayer = new CastPlayer(mockCastContext); castPlayer.addListener(mockListener); verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture()); @@ -70,9 +81,8 @@ public class CastPlayerTest { } @Test - public void testSetPlayWhenReady_masksLocalState() { + public void testSetPlayWhenReady_masksRemoteState() { when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); - // Initially paused. assertThat(castPlayer.getPlayWhenReady()).isFalse(); castPlayer.setPlayWhenReady(true); @@ -82,20 +92,19 @@ public class CastPlayerTest { // There is a status update in the middle, which should be hidden by masking. remoteMediaClientListener.onStatusUpdated(); - Mockito.verifyNoMoreInteractions(mockListener); + verifyNoMoreInteractions(mockListener); - // Upon result, the remoteMediaClient has updated it's state according to the play() call. + // Upon result, the remoteMediaClient has updated its state according to the play() call. when(mockRemoteMediaClient.isPaused()).thenReturn(false); setResultCallbackArgumentCaptor .getValue() .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class)); - Mockito.verifyNoMoreInteractions(mockListener); + verifyNoMoreInteractions(mockListener); } @Test public void testSetPlayWhenReadyMasking_updatesUponResultChange() { when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); - // Initially paused. assertThat(castPlayer.getPlayWhenReady()).isFalse(); castPlayer.setPlayWhenReady(true); @@ -103,7 +112,7 @@ public class CastPlayerTest { assertThat(castPlayer.getPlayWhenReady()).isTrue(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); - // Upon result, the remote media client is still paused. So the state should update. + // Upon result, the remote media client is still paused. The state should reflect that. setResultCallbackArgumentCaptor .getValue() .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class)); @@ -119,4 +128,58 @@ public class CastPlayerTest { verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); assertThat(castPlayer.getPlayWhenReady()).isTrue(); } + + @Test + public void testSetRepeatMode_masksRemoteState() { + when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult); + assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); + + castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE); + verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); + assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + + // There is a status update in the middle, which should be hidden by masking. + when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL); + remoteMediaClientListener.onStatusUpdated(); + verifyNoMoreInteractions(mockListener); + + // Upon result, the mediaStatus now exposes the new repeat mode. + when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE); + setResultCallbackArgumentCaptor + .getValue() + .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class)); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void testSetRepeatMode_updatesUponResultChange() { + when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult); + + castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE); + verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); + assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + + // There is a status update in the middle, which should be hidden by masking. + when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL); + remoteMediaClientListener.onStatusUpdated(); + verifyNoMoreInteractions(mockListener); + + // Upon result, the repeat mode is ALL. The state should reflect that. + setResultCallbackArgumentCaptor + .getValue() + .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class)); + verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + } + + @Test + public void testRepeatMode_changesOnStatusUpdates() { + assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); + when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE); + remoteMediaClientListener.onStatusUpdated(); + verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + } } From 6286491621b3162b66f4e95481486244f815a4b3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 7 Nov 2019 17:50:18 +0000 Subject: [PATCH 703/807] Remove SubtitlePainter from null-checking blacklist PiperOrigin-RevId: 279107241 --- .../exoplayer2/ui/SubtitlePainter.java | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) 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 cbece8a59c..76768804df 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 @@ -35,10 +35,14 @@ import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Paints subtitle {@link Cue}s. @@ -63,9 +67,9 @@ import com.google.android.exoplayer2.util.Util; private final Paint paint; // Previous input variables. - private CharSequence cueText; - private Alignment cueTextAlignment; - private Bitmap cueBitmap; + @Nullable private CharSequence cueText; + @Nullable private Alignment cueTextAlignment; + @Nullable private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @@ -93,11 +97,11 @@ import com.google.android.exoplayer2.util.Util; private int parentBottom; // Derived drawing variables. - private StaticLayout textLayout; + private @MonotonicNonNull StaticLayout textLayout; private int textLeft; private int textTop; private int textPaddingX; - private Rect bitmapRect; + private @MonotonicNonNull Rect bitmapRect; @SuppressWarnings("ResourceType") public SubtitlePainter(Context context) { @@ -225,14 +229,18 @@ import com.google.android.exoplayer2.util.Util; this.parentBottom = cueBoxBottom; if (isTextCue) { + Assertions.checkNotNull(cueText); setupTextLayout(); } else { + Assertions.checkNotNull(cueBitmap); setupBitmapLayout(); } drawLayout(canvas, isTextCue); } + @RequiresNonNull("cueText") private void setupTextLayout() { + CharSequence cueText = this.cueText; int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -248,7 +256,6 @@ import com.google.android.exoplayer2.util.Util; return; } - CharSequence cueText = this.cueText; // Remove embedded styling or font size if requested. if (!applyEmbeddedStyles) { cueText = cueText.toString(); // Equivalent to erasing all spans. @@ -364,7 +371,9 @@ import com.google.android.exoplayer2.util.Util; this.textPaddingX = textPaddingX; } + @RequiresNonNull("cueBitmap") private void setupBitmapLayout() { + Bitmap cueBitmap = this.cueBitmap; int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; float anchorX = parentLeft + (parentWidth * cuePosition); @@ -389,6 +398,8 @@ import com.google.android.exoplayer2.util.Util; if (isTextCue) { drawTextLayout(canvas); } else { + Assertions.checkNotNull(bitmapRect); + Assertions.checkNotNull(cueBitmap); drawBitmapLayout(canvas); } } @@ -438,8 +449,9 @@ import com.google.android.exoplayer2.util.Util; canvas.restoreToCount(saveCount); } + @RequiresNonNull({"cueBitmap", "bitmapRect"}) private void drawBitmapLayout(Canvas canvas) { - canvas.drawBitmap(cueBitmap, null, bitmapRect, null); + canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, /* paint= */ null); } /** @@ -448,10 +460,10 @@ import com.google.android.exoplayer2.util.Util; * may be embedded within the {@link CharSequence}s. */ @SuppressWarnings("UndefinedEquals") - private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) { + private static boolean areCharSequencesEqual( + @Nullable CharSequence first, @Nullable CharSequence second) { // Some CharSequence implementations don't perform a cheap referential equality check in their // equals methods, so we perform one explicitly here. return first == second || (first != null && first.equals(second)); } - } From a9ef9c46c8b6602b9e31ff666a9937d3ad03cee5 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Nov 2019 19:09:40 +0000 Subject: [PATCH 704/807] Bump version to 2.11.0 Note: Release notes are not final. PiperOrigin-RevId: 279125474 --- RELEASENOTES.md | 8 +++++--- constants.gradle | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5d1d054fed..6f78676179 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,9 +2,13 @@ ### dev-v2 (not yet released) ### +### 2.11.0 (not yet released) ### + * AV1 extension: Uses libgav1 to decode AV1 videos. Android 10 includes an AV1 decoder, but the older versions of Android require this extension for playback - of AV1 streams ([#3353](https://github.com/google/ExoPlayer/issues/3353)). + of AV1 streams ([#3353](https://github.com/google/ExoPlayer/issues/3353)). You + can read more about playing AV1 videos with ExoPlayer + [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). * DRM: * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` ([#5619](https://github.com/google/ExoPlayer/issues/5619)). @@ -68,8 +72,6 @@ ([#6267](https://github.com/google/ExoPlayer/issues/6267)). * Add `uid` to `Timeline.Window` to uniquely identify window instances. * Fix Dolby Vision fallback to AVC and HEVC. -* Add top-level playlist API - ([#6161](https://github.com/google/ExoPlayer/issues/6161)). * Add demo app to show how to use the Android 10 `SurfaceControl` API with ExoPlayer ([#677](https://github.com/google/ExoPlayer/issues/677)). * Add automatic `WakeLock` handling to `SimpleExoPlayer` through calling diff --git a/constants.gradle b/constants.gradle index e957bf3f6a..decb25c666 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.7' - releaseVersionCode = 2010007 + releaseVersion = '2.11.0' + releaseVersionCode = 2011000 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 29 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 79d395a858..249ef7e44e 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.7"; + public static final String VERSION = "2.11.0"; /** 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.7"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.0"; /** * 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 = 2010007; + public static final int VERSION_INT = 2011000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 71f7ab1c57e474f390d5c075bcb9ee12f06b53bc Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Nov 2019 12:15:29 +0000 Subject: [PATCH 705/807] Workaround for pre-M platform bug when instantiating CaptioningManager. PiperOrigin-RevId: 279286802 --- .../exoplayer2/trackselection/TrackSelectionParameters.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 bc21fb2bf3..6e10171f08 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import android.annotation.TargetApi; import android.content.Context; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -171,6 +172,11 @@ public class TrackSelectionParameters implements Parcelable { @TargetApi(19) private void setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19( Context context) { + if (Util.SDK_INT < 23 && Looper.myLooper() == null) { + // Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when + // CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904]. + return; + } CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager == null || !captioningManager.isEnabled()) { From 266c13913ac3095cefdc97ebfae11888254f5919 Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 9 Nov 2019 16:35:07 +0000 Subject: [PATCH 706/807] Fix spurious regex simpliciation Android Studio claims this escaping isn't required, but now it's removed this code crashes due to a malformed regex. PiperOrigin-RevId: 279501823 --- .../java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cac29daf5b..faa015fd67 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 @@ -198,7 +198,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { String text = lineValues[formatTextIndex] - .replaceAll("\\{.*?}", "") + .replaceAll("\\{.*?\\}", "") // Warning that \\} can be replaced with } is bogus. .replaceAll("\\\\N", "\n") .replaceAll("\\\\n", "\n"); cues.add(new Cue(text)); From be03c0841043275e6952af6608b9e474fdd4b6b9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Nov 2019 04:21:57 +0000 Subject: [PATCH 707/807] Add test for becoming noisy handling To trigger receiving the broadcast it's necessary to idle() the shadow main looper, which has to be done from the test thread. Therefore this change removes the send broadcast action and instead sends the broadcast from the test thread. PiperOrigin-RevId: 279660935 --- .../android/exoplayer2/ExoPlayerTest.java | 70 +++++++++++++++++ .../android/exoplayer2/testutil/Action.java | 78 +++++++++++++------ .../exoplayer2/testutil/ActionSchedule.java | 23 +++--- 3 files changed, 136 insertions(+), 35 deletions(-) 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 7146e2e405..0871d8efc3 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 @@ -21,9 +21,11 @@ import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; import android.content.Context; +import android.content.Intent; import android.graphics.SurfaceTexture; import android.media.AudioManager; import android.net.Uri; +import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; @@ -3014,6 +3016,69 @@ public final class ExoPlayerTest { assertThat(positionMs[0]).isAtLeast(5000L); } + @Test + public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception { + CountDownLatch becomingNoisyHandlingDisabled = new CountDownLatch(1); + CountDownLatch becomingNoisyDelivered = new CountDownLatch(1); + PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setHandleAudioBecomingNoisy(false); + becomingNoisyHandlingDisabled.countDown(); + + // Wait for the broadcast to be delivered from the main thread. + try { + becomingNoisyDelivered.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + }) + .delay(1) // Handle pending messages on the playback thread. + .executeRunnable(playerStateGrabber) + .build(); + + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder().setActionSchedule(actionSchedule).build(context).start(); + becomingNoisyHandlingDisabled.await(); + deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + becomingNoisyDelivered.countDown(); + + testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + assertThat(playerStateGrabber.playWhenReady).isTrue(); + } + + @Test + public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception { + CountDownLatch becomingNoisyHandlingEnabled = new CountDownLatch(1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setHandleAudioBecomingNoisy(true); + becomingNoisyHandlingEnabled.countDown(); + } + }) + .waitForPlayWhenReady(false) // Becoming noisy should set playWhenReady = false + .play() + .build(); + + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder().setActionSchedule(actionSchedule).build(context).start(); + becomingNoisyHandlingEnabled.await(); + deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + + // If the player fails to handle becoming noisy, blockUntilActionScheduleFinished will time out + // and throw, causing the test to fail. + testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { @@ -3036,6 +3101,11 @@ public final class ExoPlayerTest { }); } + private static void deliverBroadcast(Intent intent) { + ApplicationProvider.getApplicationContext().sendBroadcast(intent); + shadowOf(Looper.getMainLooper()).idle(); + } + // Internal classes. private static final class PositionGrabbingMessageTarget extends PlayerTarget { 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 7936f9b51c..b65accdf3f 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 @@ -15,11 +15,9 @@ */ package com.google.android.exoplayer2.testutil; -import android.content.Intent; import android.os.Handler; import android.view.Surface; import androidx.annotation.Nullable; -import androidx.test.core.app.ApplicationProvider; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -213,26 +211,6 @@ public abstract class Action { } - /** Broadcasts an {@link Intent}. */ - public static final class SendBroadcast extends Action { - private final Intent intent; - - /** - * @param tag A tag to use for logging. - * @param intent The {@link Intent} to broadcast. - */ - public SendBroadcast(String tag, Intent intent) { - super(tag, "SendBroadcast: " + intent.getAction()); - this.intent = intent; - } - - @Override - protected void doActionImpl( - SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { - ApplicationProvider.getApplicationContext().sendBroadcast(intent); - } - } - /** * Updates the {@link Parameters} of a {@link DefaultTrackSelector} to specify whether the * renderer at a given index should be disabled. @@ -650,6 +628,57 @@ public abstract class Action { } } + /** + * Waits for a specified playWhenReady value, returning either immediately or after a call to + * {@link Player.EventListener#onPlayerStateChanged(boolean, int)}. + */ + public static final class WaitForPlayWhenReady extends Action { + + private final boolean targetPlayWhenReady; + + /** + * @param tag A tag to use for logging. + * @param playWhenReady The playWhenReady value to wait for. + */ + public WaitForPlayWhenReady(String tag, boolean playWhenReady) { + super(tag, "WaitForPlayWhenReady"); + targetPlayWhenReady = playWhenReady; + } + + @Override + protected void doActionAndScheduleNextImpl( + SimpleExoPlayer player, + DefaultTrackSelector trackSelector, + Surface surface, + HandlerWrapper handler, + ActionNode nextAction) { + if (nextAction == null) { + return; + } + if (targetPlayWhenReady == player.getPlayWhenReady()) { + nextAction.schedule(player, trackSelector, surface, handler); + } else { + player.addListener( + new Player.EventListener() { + @Override + public void onPlayerStateChanged( + boolean playWhenReady, @Player.State int playbackState) { + if (targetPlayWhenReady == playWhenReady) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }); + } + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + // Not triggered. + } + } + /** * Waits for a specified playback state, returning either immediately or after a call to {@link * Player.EventListener#onPlayerStateChanged(boolean, int)}. @@ -658,7 +687,10 @@ public abstract class Action { private final int targetPlaybackState; - /** @param tag A tag to use for logging. */ + /** + * @param tag A tag to use for logging. + * @param targetPlaybackState The playback state to wait for. + */ public WaitForPlaybackState(String tag, int targetPlaybackState) { super(tag, "WaitForPlaybackState"); this.targetPlaybackState = targetPlaybackState; 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 b1ab3f94bb..f6ab4b9924 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.content.Intent; import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; @@ -34,7 +33,6 @@ import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; -import com.google.android.exoplayer2.testutil.Action.SendBroadcast; import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; @@ -46,6 +44,7 @@ import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading; +import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; @@ -389,16 +388,6 @@ public final class ActionSchedule { return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); } - /** - * Schedules broadcasting an {@link Intent}. - * - * @param intent An intent to broadcast. - * @return The builder, for convenience. - */ - public Builder sendBroadcast(Intent intent) { - return apply(new SendBroadcast(tag, intent)); - } - /** * Schedules a delay until any timeline change. * @@ -428,6 +417,16 @@ public final class ActionSchedule { return apply(new WaitForPositionDiscontinuity(tag)); } + /** + * Schedules a delay until playWhenReady has the specified value. + * + * @param targetPlayWhenReady The target playWhenReady value. + * @return The builder, for convenience. + */ + public Builder waitForPlayWhenReady(boolean targetPlayWhenReady) { + return apply(new WaitForPlayWhenReady(tag, targetPlayWhenReady)); + } + /** * Schedules a delay until the playback state changed to the specified state. * From 03511776116bc6a02c684b1278bf3faa543d283b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Nov 2019 05:33:45 +0000 Subject: [PATCH 708/807] Handle new signaling for E-AC3 JOC in DASH Issue: #6636 PiperOrigin-RevId: 279666771 --- RELEASENOTES.md | 2 ++ .../exoplayer2/source/dash/manifest/DashManifestParser.java | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6f78676179..06f925a68d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,6 +109,8 @@ the extension's readme. * Add support for subtitle files to the demo app ([#5523](https://github.com/google/ExoPlayer/issues/5523)). +* Handle new signaling for E-AC3 JOC audio in DASH + ([#6636](https://github.com/google/ExoPlayer/issues/6636)). ### 2.10.7 (2019-11-12) ### 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 643b1203dc..ff00c5b0d4 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 @@ -1477,8 +1477,10 @@ public class DashManifestParser extends DefaultHandler for (int i = 0; i < supplementalProperties.size(); i++) { Descriptor descriptor = supplementalProperties.get(i); String schemeIdUri = descriptor.schemeIdUri; - if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) - && "ec+3".equals(descriptor.value)) { + if (("tag:dolby.com,2018:dash:EC3_ExtensionComplexityIndex:2018".equals(schemeIdUri) + && "JOC".equals(descriptor.value)) + || ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value))) { return MimeTypes.AUDIO_E_AC3_JOC; } } From 13cf2360c88d35bef36f97183a12eb6e8746524a Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 11 Nov 2019 14:22:30 +0000 Subject: [PATCH 709/807] Compute format maxInputSize in FlacReader Use the maximum frame size as the maximum sample size if provided. PiperOrigin-RevId: 279722820 --- .../google/android/exoplayer2/extractor/ogg/FlacReader.java | 4 +++- library/core/src/test/assets/ogg/bear_flac.ogg.0.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.1.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.2.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.3.dump | 2 +- library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump | 2 +- .../core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump | 2 +- .../src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump | 2 +- 11 files changed, 13 insertions(+), 11 deletions(-) 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 cc536acb14..cef274b903 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 @@ -72,6 +72,8 @@ import java.util.List; byte[] data = packet.data; if (streamMetadata == null) { streamMetadata = new FlacStreamMetadata(data, 17); + int maxInputSize = + streamMetadata.maxFrameSize == 0 ? Format.NO_VALUE : streamMetadata.maxFrameSize; 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); @@ -81,7 +83,7 @@ import java.util.List; MimeTypes.AUDIO_FLAC, /* codecs= */ null, streamMetadata.bitRate(), - /* maxInputSize= */ Format.NO_VALUE, + maxInputSize, streamMetadata.channels, streamMetadata.sampleRate, initializationData, diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump index ccd09181a8..365040c46c 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump index 0f36b2b32e..ff020b32fd 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump index b3ff58e707..88deeaebd3 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump index ea2eff8b04..2eb7be2454 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump index ccd09181a8..365040c46c 100644 --- a/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump index 0972d17f2f..c07b2f3844 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump index e33b81c90f..a7fce3c901 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump index b8b7d85393..d05d36bd1e 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump index c866017548..376cb68499 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index 735d97eed1..44a93a6037 100644 --- a/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/flac - maxInputSize = -1 + maxInputSize = 5776 width = -1 height = -1 frameRate = -1.0 From 844c023b654bdedf093af4b5c3ed5aafe218b877 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 11 Nov 2019 15:01:04 +0000 Subject: [PATCH 710/807] Add CRC-8 method in Util PiperOrigin-RevId: 279727618 --- .../extractor/ts/SectionReader.java | 2 +- .../google/android/exoplayer2/util/Util.java | 123 ++++++++++++------ .../android/exoplayer2/util/UtilTest.java | 24 ++++ 3 files changed, 107 insertions(+), 42 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index 101a1f74d9..bc590c9d4c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -114,7 +114,7 @@ public final class SectionReader implements TsPayloadReader { if (bytesRead == totalSectionLength) { if (sectionSyntaxIndicator) { // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11. - if (Util.crc(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { + if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { // The CRC is invalid so discard the section. waitingForPayloadStart = true; return; 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 ec022eaac3..50ba7ba1d9 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 @@ -1719,8 +1719,8 @@ public final class Util { } /** - * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" - * order. + * Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit + * first" order. * * @param bytes Array containing the bytes to update the crc value with. * @param start The index to the first byte in the byte range to update the crc with. @@ -1728,7 +1728,7 @@ public final class Util { * @param initialValue The initial value for the crc calculation. * @return The result of updating the initial value with the specified bytes. */ - public static int crc(byte[] bytes, int start, int end, int initialValue) { + public static int crc32(byte[] bytes, int start, int end, int initialValue) { for (int i = start; i < end; i++) { initialValue = (initialValue << 8) ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF]; @@ -1736,6 +1736,23 @@ public final class Util { return initialValue; } + /** + * Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit + * first" order. + * + * @param bytes Array containing the bytes to update the crc value with. + * @param start The index to the first byte in the byte range to update the crc with. + * @param end The index after the last byte in the byte range to update the crc with. + * @param initialValue The initial value for the crc calculation. + * @return The result of updating the initial value with the specified bytes. + */ + public static int crc8(byte[] bytes, int start, int end, int initialValue) { + for (int i = start; i < end; i++) { + initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)]; + } + return initialValue; + } + /** * Returns the {@link C.NetworkType} of the current network connection. * @@ -2096,47 +2113,71 @@ public final class Util { }; /** - * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order + * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". */ private static final int[] CRC32_BYTES_MSBF = { - 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, - 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, - 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, - 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, - 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, - 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, - 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, - 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, - 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, - 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, - 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, - 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, - 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, - 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, - 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, - 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, - 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, - 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, - 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, - 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, - 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, - 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, - 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, - 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, - 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, - 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, - 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, - 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, - 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, - 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, - 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, - 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, - 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, - 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, - 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, - 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, - 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 + 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, + 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, + 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, + 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, + 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, + 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, + 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, + 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, + 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, + 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, + 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, + 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, + 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, + 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, + 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, + 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, + 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, + 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, + 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, + 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, + 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, + 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, + 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, + 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, + 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, + 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, + 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, + 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, + 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, + 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, + 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, + 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, + 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, + 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, + 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, + 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, + 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 }; + /** + * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit being the order + * "most significant bit first". + */ + private static final int[] CRC8_BYTES_MSBF = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, + 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, + 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, + 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, + 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, + 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, + 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, + 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, + 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, + 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, + 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, + 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, + 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40, + 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, + 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, + 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, + 0xF3 + }; } 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 b54bb0b160..dd86655d8a 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 @@ -266,6 +266,30 @@ public class UtilTest { } } + @Test + public void testCrc32() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0xFFFFFFFF; + + int result = Util.crc32(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x67ce9747); + } + + @Test + public void testCrc8() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0; + + int result = Util.crc8(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x4); + } + @Test public void testInflate() { byte[] testData = TestUtil.buildTestData(/*arbitrary test data size*/ 256 * 1024); From e49f1f8a61a4344ed092ae50654414943500a849 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 11 Nov 2019 15:42:20 +0000 Subject: [PATCH 711/807] Rollback of https://github.com/google/ExoPlayer/commit/844c023b654bdedf093af4b5c3ed5aafe218b877 *** Original commit *** Add CRC-8 method in Util *** PiperOrigin-RevId: 279733541 --- .../extractor/ts/SectionReader.java | 2 +- .../google/android/exoplayer2/util/Util.java | 123 ++++++------------ .../android/exoplayer2/util/UtilTest.java | 24 ---- 3 files changed, 42 insertions(+), 107 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index bc590c9d4c..101a1f74d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -114,7 +114,7 @@ public final class SectionReader implements TsPayloadReader { if (bytesRead == totalSectionLength) { if (sectionSyntaxIndicator) { // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11. - if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { + if (Util.crc(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { // The CRC is invalid so discard the section. waitingForPayloadStart = true; return; 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 50ba7ba1d9..ec022eaac3 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 @@ -1719,8 +1719,8 @@ public final class Util { } /** - * Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit - * first" order. + * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" + * order. * * @param bytes Array containing the bytes to update the crc value with. * @param start The index to the first byte in the byte range to update the crc with. @@ -1728,7 +1728,7 @@ public final class Util { * @param initialValue The initial value for the crc calculation. * @return The result of updating the initial value with the specified bytes. */ - public static int crc32(byte[] bytes, int start, int end, int initialValue) { + public static int crc(byte[] bytes, int start, int end, int initialValue) { for (int i = start; i < end; i++) { initialValue = (initialValue << 8) ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF]; @@ -1736,23 +1736,6 @@ public final class Util { return initialValue; } - /** - * Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit - * first" order. - * - * @param bytes Array containing the bytes to update the crc value with. - * @param start The index to the first byte in the byte range to update the crc with. - * @param end The index after the last byte in the byte range to update the crc with. - * @param initialValue The initial value for the crc calculation. - * @return The result of updating the initial value with the specified bytes. - */ - public static int crc8(byte[] bytes, int start, int end, int initialValue) { - for (int i = start; i < end; i++) { - initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)]; - } - return initialValue; - } - /** * Returns the {@link C.NetworkType} of the current network connection. * @@ -2113,71 +2096,47 @@ public final class Util { }; /** - * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit being the order + * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". */ private static final int[] CRC32_BYTES_MSBF = { - 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, - 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, - 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, - 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, - 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, - 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, - 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, - 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, - 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, - 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, - 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, - 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, - 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, - 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, - 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, - 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, - 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, - 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, - 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, - 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, - 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, - 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, - 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, - 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, - 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, - 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, - 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, - 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, - 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, - 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, - 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, - 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, - 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, - 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, - 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, - 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, - 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 + 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, + 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, + 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, + 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, + 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, + 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, + 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, + 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, + 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, + 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, + 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, + 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, + 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, + 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, + 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, + 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, + 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, + 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, + 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, + 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, + 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, + 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, + 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, + 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, + 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, + 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, + 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, + 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, + 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, + 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, + 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, + 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, + 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, + 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, + 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, + 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, + 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 }; - /** - * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit being the order - * "most significant bit first". - */ - private static final int[] CRC8_BYTES_MSBF = { - 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, - 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, - 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, - 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, - 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, - 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, - 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, - 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, - 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, - 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, - 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, - 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, - 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40, - 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, - 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, - 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, - 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, - 0xF3 - }; } 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 dd86655d8a..b54bb0b160 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 @@ -266,30 +266,6 @@ public class UtilTest { } } - @Test - public void testCrc32() { - byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; - int start = 1; - int end = 4; - int initialValue = 0xFFFFFFFF; - - int result = Util.crc32(bytes, start, end, initialValue); - - assertThat(result).isEqualTo(0x67ce9747); - } - - @Test - public void testCrc8() { - byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; - int start = 1; - int end = 4; - int initialValue = 0; - - int result = Util.crc8(bytes, start, end, initialValue); - - assertThat(result).isEqualTo(0x4); - } - @Test public void testInflate() { byte[] testData = TestUtil.buildTestData(/*arbitrary test data size*/ 256 * 1024); From b477d8b68a56c3f3d2c186657a6e7af5c4312cb2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 Nov 2019 00:18:44 +0000 Subject: [PATCH 712/807] Don't check channels for E-AC3 JOC passthrough PiperOrigin-RevId: 279841132 --- .../android/exoplayer2/audio/MediaCodecAudioRenderer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 21d2bee056..e362697179 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 @@ -541,7 +541,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @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)) { + // E-AC3 JOC is object-based so the output channel count is arbitrary. + if (audioSink.supportsOutput(/* channelCount= */ Format.NO_VALUE, 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. From 4fd881a5516a04409cc0f54d4e6be6c059519f79 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Nov 2019 06:53:39 +0000 Subject: [PATCH 713/807] Suppress warnings emitted by Checker Framework version 3.0.0 More information: https://docs.google.com/document/d/16tpK6aXqN68PvTyvt4siM-m7f0NXi_8xEeitLDzr8xY/edit?usp=sharing Tested: TAP --sample ran all affected tests and none failed http://test/OCL:279845168:BASE:279870402:1573537714395:80ca701c PiperOrigin-RevId: 279891832 --- .../google/android/exoplayer2/ui/PlayerNotificationManager.java | 2 ++ 1 file changed, 2 insertions(+) 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 9decf900a7..569fc93456 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 @@ -1006,6 +1006,8 @@ public class PlayerNotificationManager { * NotificationCompat.Builder#build()} to obtain the notification, or {@code null} if no * notification should be displayed. */ + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") @Nullable protected NotificationCompat.Builder createNotification( Player player, From 6df92fdd3f8039b7c7508d48bc78b8f6b7b72702 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Nov 2019 08:06:09 +0000 Subject: [PATCH 714/807] Update VP9 extension README to ref NDK r20 PiperOrigin-RevId: 279899984 --- extensions/vp9/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 1377f74f2a..03d9b7413d 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,7 +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. + This build configuration has been tested on NDK r20. ``` NDK_PATH="" From 2faef483026114a684406710561937b5ed15228c Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 12 Nov 2019 09:33:59 +0000 Subject: [PATCH 715/807] Add CRC-8 method in Util PiperOrigin-RevId: 279911378 --- .../extractor/ts/SectionReader.java | 2 +- .../google/android/exoplayer2/util/Util.java | 125 ++++++++++++------ .../android/exoplayer2/util/UtilTest.java | 24 ++++ 3 files changed, 108 insertions(+), 43 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index 101a1f74d9..bc590c9d4c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -114,7 +114,7 @@ public final class SectionReader implements TsPayloadReader { if (bytesRead == totalSectionLength) { if (sectionSyntaxIndicator) { // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11. - if (Util.crc(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { + if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { // The CRC is invalid so discard the section. waitingForPayloadStart = true; return; 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 ec022eaac3..a6e49fd645 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 @@ -1719,8 +1719,8 @@ public final class Util { } /** - * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" - * order. + * Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit + * first" order. * * @param bytes Array containing the bytes to update the crc value with. * @param start The index to the first byte in the byte range to update the crc with. @@ -1728,7 +1728,7 @@ public final class Util { * @param initialValue The initial value for the crc calculation. * @return The result of updating the initial value with the specified bytes. */ - public static int crc(byte[] bytes, int start, int end, int initialValue) { + public static int crc32(byte[] bytes, int start, int end, int initialValue) { for (int i = start; i < end; i++) { initialValue = (initialValue << 8) ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF]; @@ -1736,6 +1736,23 @@ public final class Util { return initialValue; } + /** + * Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit + * first" order. + * + * @param bytes Array containing the bytes to update the crc value with. + * @param start The index to the first byte in the byte range to update the crc with. + * @param end The index after the last byte in the byte range to update the crc with. + * @param initialValue The initial value for the crc calculation. + * @return The result of updating the initial value with the specified bytes. + */ + public static int crc8(byte[] bytes, int start, int end, int initialValue) { + for (int i = start; i < end; i++) { + initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)]; + } + return initialValue; + } + /** * Returns the {@link C.NetworkType} of the current network connection. * @@ -2096,47 +2113,71 @@ public final class Util { }; /** - * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order - * "most significant bit first". + * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit in the order "most + * significant bit first". */ private static final int[] CRC32_BYTES_MSBF = { - 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, - 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, - 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, - 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, - 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, - 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, - 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, - 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, - 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, - 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, - 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, - 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, - 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, - 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, - 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, - 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, - 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, - 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, - 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, - 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, - 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, - 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, - 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, - 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, - 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, - 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, - 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, - 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, - 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, - 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, - 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, - 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, - 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, - 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, - 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, - 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, - 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 + 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, + 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, + 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, + 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, + 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, + 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, + 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, + 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, + 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, + 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, + 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, + 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, + 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, + 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, + 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, + 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, + 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, + 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, + 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, + 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, + 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, + 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, + 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, + 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, + 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, + 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, + 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, + 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, + 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, + 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, + 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, + 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, + 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, + 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, + 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, + 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, + 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 }; + /** + * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit in the order "most + * significant bit first". + */ + private static final int[] CRC8_BYTES_MSBF = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, + 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, + 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, + 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, + 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, + 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, + 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, + 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, + 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, + 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, + 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, + 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, + 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40, + 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, + 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, + 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, + 0xF3 + }; } 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 b54bb0b160..172fdf31ad 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 @@ -266,6 +266,30 @@ public class UtilTest { } } + @Test + public void testCrc32() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0xFFFFFFFF; + + int result = Util.crc32(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x67CE9747); + } + + @Test + public void testCrc8() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0; + + int result = Util.crc8(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x4); + } + @Test public void testInflate() { byte[] testData = TestUtil.buildTestData(/*arbitrary test data size*/ 256 * 1024); From b43db3bceb3e9c157f112cea74e7fe629bab13d1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 12 Nov 2019 10:54:49 +0000 Subject: [PATCH 716/807] Clarify SSA and SubRip docs in MatroskaExtractor The handling of times wasn't really clear to me, hopefully this more exhaustive documentation helps a bit. Also assert the end timecode is the 'correct' length for the format. PiperOrigin-RevId: 279922369 --- .../extractor/mkv/MatroskaExtractor.java | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 56ba01b967..d4849968bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -236,14 +236,21 @@ public class MatroskaExtractor implements Extractor { private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; /** - * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode - * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be - * replaced with the duration of the subtitle. - *

        - * Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n". + * A template for the prefix that must be added to each subrip sample. + * + *

        The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with + * the duration of the subtitle. + * + *

        Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n". */ - private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48, - 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10}; + private static final byte[] SUBRIP_PREFIX = + new byte[] { + 49, 10, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, + 48, 58, 48, 48, 44, 48, 48, 48, 10 + }; /** * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. */ @@ -272,14 +279,21 @@ public class MatroskaExtractor implements Extractor { private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, " + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); /** - * A template for the prefix that must be added to each SSA sample. The 10 byte end timecode - * starting at {@link #SSA_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be - * replaced with the duration of the subtitle. - *

        - * Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,". + * A template for the prefix that must be added to each SSA sample. + * + *

        The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with + * the duration of the subtitle. + * + *

        Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,". */ - private static final byte[] SSA_PREFIX = new byte[] {68, 105, 97, 108, 111, 103, 117, 101, 58, 32, - 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44}; + private static final byte[] SSA_PREFIX = + new byte[] { + 68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, + 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44 + }; /** * The byte offset of the end timecode in {@link #SSA_PREFIX}. */ @@ -1468,16 +1482,32 @@ public class MatroskaExtractor implements Extractor { private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor, byte[] emptyTimecode) { - setSampleDuration(subtitleSample.data, blockDurationUs, timecodeFormat, endTimecodeOffset, - lastTimecodeValueScalingFactor, emptyTimecode); + setSubtitleSampleDuration( + subtitleSample.data, + blockDurationUs, + timecodeFormat, + endTimecodeOffset, + lastTimecodeValueScalingFactor, + emptyTimecode); // Note: If we ever want to support DRM protected subtitles then we'll need to output the // appropriate encryption data here. track.output.sampleData(subtitleSample, subtitleSample.limit()); sampleBytesWritten += subtitleSample.limit(); } - private static void setSampleDuration(byte[] subripSampleData, long durationUs, - String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor, + /** + * Formats {@code durationUs} using {@code timecodeFormat}, and sets it as the end timecode in + * {@code subtitleSampleData}. + * + *

        See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use + * the duration as the end timecode. + */ + private static void setSubtitleSampleDuration( + byte[] subtitleSampleData, + long durationUs, + String timecodeFormat, + int endTimecodeOffset, + long lastTimecodeValueScalingFactor, byte[] emptyTimecode) { byte[] timeCodeData; if (durationUs == C.TIME_UNSET) { @@ -1493,7 +1523,8 @@ public class MatroskaExtractor implements Extractor { timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); } - System.arraycopy(timeCodeData, 0, subripSampleData, endTimecodeOffset, emptyTimecode.length); + Assertions.checkState(timeCodeData.length == emptyTimecode.length); + System.arraycopy(timeCodeData, 0, subtitleSampleData, endTimecodeOffset, timeCodeData.length); } /** From ddb70d96ad99f07fe10f53a76ce3262fe625be70 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 12 Nov 2019 11:23:54 +0000 Subject: [PATCH 717/807] Require an end timecode in SSA and Subrip subtitles SSA spec allows the lines in any order, so they must all have an end time: http://moodub.free.fr/video/ass-specs.doc The Matroska write-up of SubRip assumes the end time is present: https://matroska.org/technical/specs/subtitles/srt.html This will massively simplify merging issue:#6595 PiperOrigin-RevId: 279926730 --- RELEASENOTES.md | 3 + .../extractor/mkv/MatroskaExtractor.java | 122 ++++++++---------- .../exoplayer2/text/ssa/SsaDecoder.java | 20 +-- .../exoplayer2/text/subrip/SubripDecoder.java | 15 +-- .../src/test/assets/ssa/invalid_timecodes | 2 + .../core/src/test/assets/ssa/no_end_timecodes | 12 -- .../src/test/assets/subrip/no_end_timecodes | 11 -- .../assets/subrip/typical_missing_timecode | 8 ++ .../exoplayer2/text/ssa/SsaDecoderTest.java | 23 ---- .../text/subrip/SubripDecoderTest.java | 23 ---- 10 files changed, 79 insertions(+), 160 deletions(-) delete mode 100644 library/core/src/test/assets/ssa/no_end_timecodes delete mode 100644 library/core/src/test/assets/subrip/no_end_timecodes diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 06f925a68d..8b8f69f6a6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -111,6 +111,9 @@ ([#5523](https://github.com/google/ExoPlayer/issues/5523)). * Handle new signaling for E-AC3 JOC audio in DASH ([#6636](https://github.com/google/ExoPlayer/issues/6636)). +* Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). ### 2.10.7 (2019-11-12) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index d4849968bf..517c087e18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -255,14 +255,6 @@ public class MatroskaExtractor implements Extractor { * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. */ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; - /** - * A special end timecode indicating that a subrip subtitle should be displayed until the next - * subtitle, or until the end of the media in the case of the last subtitle. - *

        - * Equivalent to the UTF-8 string: " ". - */ - private static final byte[] SUBRIP_TIMECODE_EMPTY = - new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; /** * The value by which to divide a time in microseconds to convert it to the unit of the last value * in a subrip timecode (milliseconds). @@ -303,14 +295,6 @@ public class MatroskaExtractor implements Extractor { * in an SSA timecode (1/100ths of a second). */ private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; - /** - * A special end timecode indicating that an SSA subtitle should be displayed until the next - * subtitle, or until the end of the media in the case of the last subtitle. - *

        - * Equivalent to the UTF-8 string: " ". - */ - private static final byte[] SSA_TIMECODE_EMPTY = - new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; /** * The format of an SSA timecode. */ @@ -1238,21 +1222,18 @@ public class MatroskaExtractor implements Extractor { if (track.trueHdSampleRechunker != null) { track.trueHdSampleRechunker.sampleMetadata(track, timeUs); } else { - if (CODEC_ID_SUBRIP.equals(track.codecId)) { - commitSubtitleSample( - track, - SUBRIP_TIMECODE_FORMAT, - SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, - SUBRIP_TIMECODE_EMPTY); - } else if (CODEC_ID_ASS.equals(track.codecId)) { - commitSubtitleSample( - track, - SSA_TIMECODE_FORMAT, - SSA_PREFIX_END_TIMECODE_OFFSET, - SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, - SSA_TIMECODE_EMPTY); + if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { + if (durationUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping subtitle sample with no duration."); + } else { + setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); + // Note: If we ever want to support DRM protected subtitles then we'll need to output the + // appropriate encryption data here. + track.output.sampleData(subtitleSample, subtitleSample.limit()); + sampleBytesWritten += subtitleSample.limit(); + } } + if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { // Append supplemental data. int size = blockAddData.limit(); @@ -1480,51 +1461,58 @@ public class MatroskaExtractor implements Extractor { // the correct end timecode, which we might not have yet. } - private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset, - long lastTimecodeValueScalingFactor, byte[] emptyTimecode) { - setSubtitleSampleDuration( - subtitleSample.data, - blockDurationUs, - timecodeFormat, - endTimecodeOffset, - lastTimecodeValueScalingFactor, - emptyTimecode); - // Note: If we ever want to support DRM protected subtitles then we'll need to output the - // appropriate encryption data here. - track.output.sampleData(subtitleSample, subtitleSample.limit()); - sampleBytesWritten += subtitleSample.limit(); - } - /** - * Formats {@code durationUs} using {@code timecodeFormat}, and sets it as the end timecode in - * {@code subtitleSampleData}. + * Overwrites the end timecode in {@code subtitleData} with the correctly formatted time derived + * from {@code durationUs}. * *

        See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use * the duration as the end timecode. + * + * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}. + * @param durationUs The duration of the sample, in microseconds. + * @param subtitleData The subtitle sample in which to overwrite the end timecode (output + * parameter). */ - private static void setSubtitleSampleDuration( - byte[] subtitleSampleData, - long durationUs, - String timecodeFormat, - int endTimecodeOffset, - long lastTimecodeValueScalingFactor, - byte[] emptyTimecode) { + private static void setSubtitleEndTime(String codecId, long durationUs, byte[] subtitleData) { + byte[] endTimecode; + int endTimecodeOffset; + switch (codecId) { + case CODEC_ID_SUBRIP: + endTimecode = + formatSubtitleTimecode( + durationUs, SUBRIP_TIMECODE_FORMAT, SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SUBRIP_PREFIX_END_TIMECODE_OFFSET; + break; + case CODEC_ID_ASS: + endTimecode = + formatSubtitleTimecode( + durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET; + break; + default: + throw new IllegalArgumentException(); + } + System.arraycopy(endTimecode, 0, subtitleData, endTimecodeOffset, endTimecode.length); + } + + /** + * Formats {@code timeUs} using {@code timecodeFormat}, and sets it as the end timecode in {@code + * subtitleSampleData}. + */ + private static byte[] formatSubtitleTimecode( + long timeUs, String timecodeFormat, long lastTimecodeValueScalingFactor) { + Assertions.checkArgument(timeUs != C.TIME_UNSET); byte[] timeCodeData; - if (durationUs == C.TIME_UNSET) { - timeCodeData = emptyTimecode; - } else { - int hours = (int) (durationUs / (3600 * C.MICROS_PER_SECOND)); - durationUs -= (hours * 3600 * C.MICROS_PER_SECOND); - int minutes = (int) (durationUs / (60 * C.MICROS_PER_SECOND)); - durationUs -= (minutes * 60 * C.MICROS_PER_SECOND); - int seconds = (int) (durationUs / C.MICROS_PER_SECOND); - durationUs -= (seconds * C.MICROS_PER_SECOND); - int lastValue = (int) (durationUs / lastTimecodeValueScalingFactor); + int hours = (int) (timeUs / (3600 * C.MICROS_PER_SECOND)); + timeUs -= (hours * 3600 * C.MICROS_PER_SECOND); + int minutes = (int) (timeUs / (60 * C.MICROS_PER_SECOND)); + timeUs -= (minutes * 60 * C.MICROS_PER_SECOND); + int seconds = (int) (timeUs / C.MICROS_PER_SECOND); + timeUs -= (seconds * C.MICROS_PER_SECOND); + int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); - } - Assertions.checkState(timeCodeData.length == emptyTimecode.length); - System.arraycopy(timeCodeData, 0, subtitleSampleData, endTimecodeOffset, timeCodeData.length); + return timeCodeData; } /** 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 faa015fd67..2e78b433bd 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 @@ -180,20 +180,16 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return; } - long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); + long startTimeUs = parseTimecodeUs(lineValues[formatStartIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } - long endTimeUs = C.TIME_UNSET; - String endTimeString = lineValues[formatEndIndex]; - if (!endTimeString.trim().isEmpty()) { - endTimeUs = SsaDecoder.parseTimecodeUs(endTimeString); - if (endTimeUs == C.TIME_UNSET) { - Log.w(TAG, "Skipping invalid timing: " + dialogueLine); - return; - } + long endTimeUs = parseTimecodeUs(lineValues[formatEndIndex]); + if (endTimeUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping invalid timing: " + dialogueLine); + return; } String text = @@ -203,10 +199,8 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { .replaceAll("\\\\n", "\n"); cues.add(new Cue(text)); cueTimesUs.add(startTimeUs); - if (endTimeUs != C.TIME_UNSET) { - cues.add(Cue.EMPTY); - cueTimesUs.add(endTimeUs); - } + cues.add(Cue.EMPTY); + cueTimesUs.add(endTimeUs); } /** 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 8d1b743a6d..20b7efe50a 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 @@ -43,7 +43,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"; private static final Pattern SUBRIP_TIMING_LINE = - Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")?\\s*"); + Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*"); private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}"); private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; @@ -90,7 +90,6 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } // Read and parse the timing line. - boolean haveEndTimecode = false; currentLine = subripData.readLine(); if (currentLine == null) { Log.w(TAG, "Unexpected end"); @@ -99,11 +98,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); if (matcher.matches()) { - cueTimesUs.add(parseTimecode(matcher, 1)); - if (!TextUtils.isEmpty(matcher.group(6))) { - haveEndTimecode = true; - cueTimesUs.add(parseTimecode(matcher, 6)); - } + cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1)); + cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6)); } else { Log.w(TAG, "Skipping invalid timing: " + currentLine); continue; @@ -133,10 +129,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } } cues.add(buildCue(text, alignmentTag)); - - if (haveEndTimecode) { - cues.add(Cue.EMPTY); - } + cues.add(Cue.EMPTY); } Cue[] cuesArray = new Cue[cues.size()]; diff --git a/library/core/src/test/assets/ssa/invalid_timecodes b/library/core/src/test/assets/ssa/invalid_timecodes index 10ebfc3109..380d330a86 100644 --- a/library/core/src/test/assets/ssa/invalid_timecodes +++ b/library/core/src/test/assets/ssa/invalid_timecodes @@ -10,3 +10,5 @@ Format: Layer, Start, End, Style, Name, Text Dialogue: 0,Invalid,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. Dialogue: 0,0:00:02.34,Invalid,Default,Olly,This is the second subtitle \nwith a newline \Nand another. Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. +Dialogue: 0, ,0:00:10:90,Default,Olly,This is the fourth subtitle. +Dialogue: 0,0:00:12:90, ,Default,Olly,This is the fifth subtitle. diff --git a/library/core/src/test/assets/ssa/no_end_timecodes b/library/core/src/test/assets/ssa/no_end_timecodes deleted file mode 100644 index b949179533..0000000000 --- a/library/core/src/test/assets/ssa/no_end_timecodes +++ /dev/null @@ -1,12 +0,0 @@ -[Script Info] -Title: SomeTitle - -[V4+ Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1 - -[Events] -Format: Layer, Start, End, Style, Name, Text -Dialogue: 0,0:00:00.00, ,Default,Olly,This is the first subtitle. -Dialogue: 0,0:00:02.34, ,Default,Olly,This is the second subtitle \nwith a newline \Nand another. -Dialogue: 0,0:00:04.56, ,Default,Olly,This is the third subtitle, with a comma. diff --git a/library/core/src/test/assets/subrip/no_end_timecodes b/library/core/src/test/assets/subrip/no_end_timecodes deleted file mode 100644 index df2c44b956..0000000000 --- a/library/core/src/test/assets/subrip/no_end_timecodes +++ /dev/null @@ -1,11 +0,0 @@ -1 -00:00:00,000 --> -SubRip doesn't technically allow missing end timecodes. - -2 -00:00:02,345 --> -We interpret it to mean that a subtitle extends to the start of the next one. - -3 -00:00:03,456 --> -Or to the end of the media. diff --git a/library/core/src/test/assets/subrip/typical_missing_timecode b/library/core/src/test/assets/subrip/typical_missing_timecode index 2c6fe69b6f..cd25ffca3b 100644 --- a/library/core/src/test/assets/subrip/typical_missing_timecode +++ b/library/core/src/test/assets/subrip/typical_missing_timecode @@ -9,3 +9,11 @@ Second subtitle with second line. 3 00:00:04,567 --> 00:00:08,901 This is the third subtitle. + +4 + --> 00:00:10,901 +This is the fourth subtitle. + +5 +00:00:12,901 --> +This is the fifth subtitle. 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 7095962801..3c48aa61dd 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 @@ -36,7 +36,6 @@ public final class SsaDecoderTest { private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue"; private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format"; private static final String INVALID_TIMECODES = "ssa/invalid_timecodes"; - private static final String NO_END_TIMECODES = "ssa/no_end_timecodes"; @Test public void testDecodeEmpty() throws IOException { @@ -92,28 +91,6 @@ public final class SsaDecoderTest { assertTypicalCue3(subtitle, 0); } - @Test - public void testDecodeNoEndTimecodes() throws IOException { - SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = - TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES); - Subtitle subtitle = decoder.decode(bytes, bytes.length, false); - - assertThat(subtitle.getEventTimeCount()).isEqualTo(3); - - assertThat(subtitle.getEventTime(0)).isEqualTo(0); - assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()) - .isEqualTo("This is the first subtitle."); - - assertThat(subtitle.getEventTime(1)).isEqualTo(2340000); - assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString()) - .isEqualTo("This is the second subtitle \nwith a newline \nand another."); - - assertThat(subtitle.getEventTime(2)).isEqualTo(4560000); - assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()) - .isEqualTo("This is the third subtitle, with a comma."); - } - private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) 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 774e8d98b9..9f66f65a56 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 @@ -39,7 +39,6 @@ public final class SubripDecoderTest { private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags"; - private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; @Test public void testDecodeEmpty() throws IOException { @@ -145,28 +144,6 @@ public final class SubripDecoderTest { assertTypicalCue2(subtitle, 2); } - @Test - public void testDecodeNoEndTimecodes() throws IOException { - SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = - TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES_FILE); - Subtitle subtitle = decoder.decode(bytes, bytes.length, false); - - assertThat(subtitle.getEventTimeCount()).isEqualTo(3); - - assertThat(subtitle.getEventTime(0)).isEqualTo(0); - assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()) - .isEqualTo("SubRip doesn't technically allow missing end timecodes."); - - assertThat(subtitle.getEventTime(1)).isEqualTo(2345000); - assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString()) - .isEqualTo("We interpret it to mean that a subtitle extends to the start of the next one."); - - assertThat(subtitle.getEventTime(2)).isEqualTo(3456000); - assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()) - .isEqualTo("Or to the end of the media."); - } - @Test public void testDecodeCueWithTag() throws IOException { SubripDecoder decoder = new SubripDecoder(); From b84a9bed2caade60a06ac5b91bd66a5f3af6449d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 12 Nov 2019 11:36:29 +0000 Subject: [PATCH 718/807] Add a parameter object for LoadErrorHandlingPolicy methods PiperOrigin-RevId: 279928178 --- .../upstream/LoadErrorHandlingPolicy.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java index 293d1e7510..68ab2a7a47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -38,6 +38,30 @@ import java.io.IOException; */ public interface LoadErrorHandlingPolicy { + /** Holds information about a load task error. */ + final class LoadErrorInfo { + + /** One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to load. */ + public final int dataType; + /** + * The duration in milliseconds of the load from the start of the first load attempt up to the + * point at which the error occurred. + */ + public final long loadDurationMs; + /** The exception associated to the load error. */ + public final IOException exception; + /** The number of errors this load task has encountered, including this one. */ + public final int errorCount; + + /** Creates an instance with the given values. */ + public LoadErrorInfo(int dataType, long loadDurationMs, IOException exception, int errorCount) { + this.dataType = dataType; + this.loadDurationMs = loadDurationMs; + this.exception = exception; + this.errorCount = errorCount; + } + } + /** * Returns the number of milliseconds for which a resource associated to a provided load error * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. @@ -54,6 +78,22 @@ public interface LoadErrorHandlingPolicy { long getBlacklistDurationMsFor( int dataType, long loadDurationMs, IOException exception, int errorCount); + /** + * Returns the number of milliseconds for which a resource associated to a provided load error + * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. + * + * @param loadErrorInfo A {@link LoadErrorInfo} holding information about the load error. + * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should + * not be blacklisted. + */ + default long getBlacklistDurationMsFor(LoadErrorInfo loadErrorInfo) { + return getBlacklistDurationMsFor( + loadErrorInfo.dataType, + loadErrorInfo.loadDurationMs, + loadErrorInfo.exception, + loadErrorInfo.errorCount); + } + /** * Returns the number of milliseconds to wait before attempting the load again, or {@link * C#TIME_UNSET} if the error is fatal and should not be retried. @@ -73,6 +113,26 @@ public interface LoadErrorHandlingPolicy { */ long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount); + /** + * Returns the number of milliseconds to wait before attempting the load again, or {@link + * C#TIME_UNSET} if the error is fatal and should not be retried. + * + *

        {@link Loader} clients may ignore the retry delay returned by this method in order to wait + * for a specific event before retrying. However, the load is retried if and only if this method + * does not return {@link C#TIME_UNSET}. + * + * @param loadErrorInfo A {@link LoadErrorInfo} holding information about the load error. + * @return The number of milliseconds to wait before attempting the load again, or {@link + * C#TIME_UNSET} if the error is fatal and should not be retried. + */ + default long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) { + return getRetryDelayMsFor( + loadErrorInfo.dataType, + loadErrorInfo.loadDurationMs, + loadErrorInfo.exception, + loadErrorInfo.errorCount); + } + /** * Returns the minimum number of times to retry a load in the case of a load error, before * propagating the error. From abe0330f39f6c3c98f5e2024dee4e640827a0a9e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 12 Nov 2019 11:38:30 +0000 Subject: [PATCH 719/807] Add a track type argument to DrmSessionManager.acquirePlaceholderSession Issue:#4867 PiperOrigin-RevId: 279928345 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 5 ++++- .../com/google/android/exoplayer2/drm/DrmSessionManager.java | 5 ++++- .../android/exoplayer2/source/SampleMetadataQueue.java | 4 +++- .../google/android/exoplayer2/source/SampleQueueTest.java | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) 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 753b8a7d3a..15f87318f0 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 @@ -458,12 +458,15 @@ public class DefaultDrmSessionManager implements DrmSe @Override @Nullable - public DrmSession acquirePlaceholderSession(Looper playbackLooper) { + public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { assertExpectedPlaybackLooper(playbackLooper); Assertions.checkNotNull(exoMediaDrm); boolean avoidPlaceholderDrmSessions = FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; + // Avoid attaching a session to sparse formats. + avoidPlaceholderDrmSessions |= + trackType != C.TRACK_TYPE_VIDEO && trackType != C.TRACK_TYPE_AUDIO; if (avoidPlaceholderDrmSessions || !preferSecureDecoders || exoMediaDrm.getExoMediaCryptoType() == null) { 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 46f9458408..1a9e01441f 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -112,11 +113,13 @@ public interface DrmSessionManager { * content periods. * * @param playbackLooper The looper associated with the media playback thread. + * @param trackType The type of the track to acquire a placeholder session for. Must be one of the + * {@link C}{@code .TRACK_TYPE_*} constants. * @return The placeholder DRM session, or null if this DRM session manager does not support * placeholder sessions. */ @Nullable - default DrmSession acquirePlaceholderSession(Looper playbackLooper) { + default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { return null; } 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 28aa5d86e0..891f36f47c 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; 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; @@ -623,7 +624,8 @@ import java.io.IOException; currentDrmSession = newDrmInitData != null ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) - : drmSessionManager.acquirePlaceholderSession(playbackLooper); + : drmSessionManager.acquirePlaceholderSession( + playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); outputFormatHolder.drmSession = currentDrmSession; if (previousSession != 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 944a998781..df6ccc1f02 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 @@ -396,7 +396,8 @@ public final class SampleQueueTest { DrmSession mockPlaceholderDrmSession = (DrmSession) Mockito.mock(DrmSession.class); when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); - when(mockDrmSessionManager.acquirePlaceholderSession(ArgumentMatchers.any())) + when(mockDrmSessionManager.acquirePlaceholderSession( + ArgumentMatchers.any(), ArgumentMatchers.anyInt())) .thenReturn(mockPlaceholderDrmSession); writeTestDataWithEncryptedSections(); From 69e51505e46d56b8d2e565bb3b1e4198abc30f47 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Nov 2019 15:32:57 +0000 Subject: [PATCH 720/807] use getPeriodByUid when searching for subsequent period of seek timeline Issue: #6641 PiperOrigin-RevId: 279963739 --- .../exoplayer2/ExoPlayerImplInternal.java | 6 +- .../android/exoplayer2/ExoPlayerTest.java | 66 ++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) 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 db5483be1d..4c25c180f4 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 @@ -1444,6 +1444,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ + @Nullable private Pair resolveSeekPosition( SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; @@ -1479,11 +1480,12 @@ import java.util.concurrent.atomic.AtomicBoolean; } if (trySubsequentPeriods) { // Try and find a subsequent period from the seek timeline in the internal timeline. + @Nullable Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); if (periodUid != null) { - // We found one. Map the SeekPosition onto the corresponding default position. + // We found one. Use the default position of the corresponding window. return getPeriodPosition( - timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET); } } // We didn't find one. Give up. 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 0871d8efc3..8bd6b1ba09 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 @@ -2168,6 +2168,70 @@ public final class ExoPlayerTest { timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); } + @Test + public void testInvalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + CountDownLatch sourceReleasedCountDownLatch = new CountDownLatch(/* count= */ 1); + MediaSource mediaSourceToRemove = + new FakeMediaSource(timeline) { + @Override + protected void releaseSourceInternal() { + super.releaseSourceInternal(); + sourceReleasedCountDownLatch.countDown(); + } + }; + ConcatenatingMediaSource mediaSource = + new ConcatenatingMediaSource(mediaSourceToRemove, new FakeMediaSource(timeline)); + final int[] windowCount = {C.INDEX_UNSET}; + final long[] position = {C.TIME_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testInvalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod") + .pause() + .waitForTimelineChanged() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + mediaSource.removeMediaSource(/* index= */ 0); + try { + // Wait until the source to be removed is released on the playback thread. So + // the timeline in EPII has one window only, but the update here in EPI is + // stuck until we finished our executable here. So when seeking below, we will + // seek in the timeline which still has two windows in EPI, but when the seek + // arrives in EPII the actual timeline has one window only. Hence it tries to + // find the subsequent period of the removed period and finds it. + sourceReleasedCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1000L); + } + }) + .waitForSeekProcessed() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + windowCount[0] = player.getCurrentTimeline().getWindowCount(); + position[0] = player.getCurrentPosition(); + } + }) + .play() + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + + // Expect the first window to be the current. + assertThat(windowCount[0]).isEqualTo(1); + // Expect the position to be in the default position. + assertThat(position[0]).isEqualTo(0L); + } + @Test public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { // We add two listeners to the player. The first stops the player as soon as it's ready and both @@ -2992,7 +3056,7 @@ public final class ExoPlayerTest { new PlayerRunnable() { @Override public void run(SimpleExoPlayer player) { - player.setMediaItem(mediaSource, /* positionMs= */ 5000); + player.setMediaItem(mediaSource, /* startPositionMs= */ 5000); player.prepare(); } }) From 20ab05103b9b5b8d7f325f60e18c886acda6cbe6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Nov 2019 22:33:47 +0000 Subject: [PATCH 721/807] First pass at finalizing 2.11.0 release notes PiperOrigin-RevId: 280056790 --- RELEASENOTES.md | 217 ++++++++++++++++++++++++++---------------------- 1 file changed, 119 insertions(+), 98 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b8f69f6a6..b4485accbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,118 +2,139 @@ ### dev-v2 (not yet released) ### +* Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). + ### 2.11.0 (not yet released) ### -* AV1 extension: Uses libgav1 to decode AV1 videos. Android 10 includes an AV1 - decoder, but the older versions of Android require this extension for playback - of AV1 streams ([#3353](https://github.com/google/ExoPlayer/issues/3353)). You - can read more about playing AV1 videos with ExoPlayer - [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). +* Core library: + * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and + `ExoPlayer.Builder`. + * Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled + by calling `SimpleExoPlayer.setHandleWakeLock` + ([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this + feature, you must add the + [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) + permission to your application's manifest file. + * Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which + can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`. + * Wrap decoder exceptions in a new `DecoderException` class and report them as + renderer errors. + * Add `Timeline.Window.isLive` to indicate that a window is a live stream + ([#2668](https://github.com/google/ExoPlayer/issues/2668) and + [#5973](https://github.com/google/ExoPlayer/issues/5973)). + * Add `Timeline.Window.uid` to uniquely identify window instances. + * Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be + set. + * Deprecate passing the manifest directly to + `Player.EventListener.onTimelineChanged`. It can be accessed through + `Timeline.Window.manifest` or `Player.getCurrentManifest()` + * Add `MediaSource.enable` and `MediaSource.disable` to improve resource + management in playlists. + * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. + * Fix issue where player errors are thrown too early at playlist transitions + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). * DRM: * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` ([#5619](https://github.com/google/ExoPlayer/issues/5619)). * Add a `DefaultDrmSessionManager.Builder`. * Add support for the use of secure decoders in clear sections of content ([#4867](https://github.com/google/ExoPlayer/issues/4867)). - * Add basic DRM support to the Cast demo app. * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). * Remove `DefaultDrmSessionManager` factory methods that leak `ExoMediaDrm` instances ([#4721](https://github.com/google/ExoPlayer/issues/4721)). -* Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Instead, set the header - `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` in the `DataSpec` - `httpRequestHeaders`. +* Track selection: + * Update `DefaultTrackSelector` to set a viewport constraint for the default + display by default. + * Update `DefaultTrackSelector` to set text language and role flag + constraints for the device's accessibility settings by default + ([#5749](https://github.com/google/ExoPlayer/issues/5749)). + * Add option to set preferred text role flags using + `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. +* Android 10: + * Set `compileSdkVersion` to 29 to enable use of Android 10 APIs. + * Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags + in `MediaCodecInfo` + ([#5839](https://github.com/google/ExoPlayer/issues/5839)). + * Add `allowedCapturePolicy` field to `AudioAttributes` to allow to + configuration of the audio capture policy. +* Video: + * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. + * Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. + * Assume that protected content requires a secure decoder when evaluating + whether `MediaCodecVideoRenderer` supports a given video format + ([#5568](https://github.com/google/ExoPlayer/issues/5568)). + * Fix Dolby Vision fallback to AVC and HEVC. +* Audio: + * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect + channel count check. + * Handle new signaling for E-AC3 JOC audio in DASH + ([#6636](https://github.com/google/ExoPlayer/issues/6636)). + * Fix the start of audio getting truncated when transitioning to a new + item in a playlist of Opus streams. + * Workaround broken raw audio decoding on Oppo R9 + ([#5782](https://github.com/google/ExoPlayer/issues/5782)). +* UI: + * Make showing and hiding player controls accessible to TalkBack in + `PlayerView`. + * Rename `spherical_view` surface type to `spherical_gl_surface_view`. +* Analytics: + * Remove `AnalyticsCollector.Factory`. Instances should be created directly, + and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. + * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and + analytics reporting (TODO: link to developer guide page/blog post). +* DataSource + * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for + HTTP and HTTPS. + * Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by + setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in + `DataSpec.httpRequestHeaders`. + * Fail more explicitly when local file URIs contain invalid parts (e.g. a + fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). * DASH: Support negative @r values in segment timelines ([#1787](https://github.com/google/ExoPlayer/issues/1787)). -* Add `allowedCapturePolicy` field to `AudioAttributes` wrapper to allow to - opt-out of audio recording. -* Add `DataSpec.httpRequestHeaders` to set HTTP request headers when connecting - to an HTTP source. `DefaultHttpDataSource`, `CronetDataSource` and - `OkHttpDataSource` include headers set in the DataSpec when connecting to the - source. -* Surface information provided by methods `isHardwareAccelerated`, - `isSoftwareOnly` and `isVendor` added in Android 10 in `MediaCodecInfo` class - ([#5839](https://github.com/google/ExoPlayer/issues/5839)). -* Update `DefaultTrackSelector` to apply a viewport constraint for the default - display by default. -* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis - and analytics reporting (TODO: link to developer guide page/blog post). -* Assume that encrypted content requires secure decoders in renderer support - checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). -* Decoders: Prefer decoders that advertise format support over ones that do not, - even if they are listed lower in the `MediaCodecList`. -* Add a workaround for broken raw audio decoding on Oppo R9 - ([#5782](https://github.com/google/ExoPlayer/issues/5782)). -* Wrap decoder exceptions in a new `DecoderException` class and report as - renderer error. -* Do not pass the manifest to callbacks of `Player.EventListener` and - `SourceInfoRefreshListener` anymore. Instead make it accessible through - `Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename - `SourceInfoRefreshListener` to `MediaSourceCaller`. -* Set `compileSdkVersion` to 29 to use Android 10 APIs. -* Add `enable` and `disable` methods to `MediaSource` to improve resource - management in playlists. -* Text selection logic: - * Allow to set preferred role flags using - `DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`. - * Default text language and role flags to accessibility captioning settings - ([#5749](https://github.com/google/ExoPlayer/issues/5749)). -* Remove `AnalyticsCollector.Factory`. Instances can be created directly and - the `Player` set later using `AnalyticsCollector.setPlayer`. -* Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and - `ExoPlayer.Builder`. -* Fix issue where player errors are thrown too early at playlist transitions - ([#5407](https://github.com/google/ExoPlayer/issues/5407)). -* Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be set. -* Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. -* Fix issue where HLS streams get stuck in infinite buffering state after - postroll ad ([#6314](https://github.com/google/ExoPlayer/issues/6314)). -* Publish `testutils` module to simplify unit testing with ExoPlayer - ([#6267](https://github.com/google/ExoPlayer/issues/6267)). -* Add `uid` to `Timeline.Window` to uniquely identify window instances. -* Fix Dolby Vision fallback to AVC and HEVC. -* Add demo app to show how to use the Android 10 `SurfaceControl` API with - ExoPlayer ([#677](https://github.com/google/ExoPlayer/issues/677)). -* Add automatic `WakeLock` handling to `SimpleExoPlayer` through calling - `setEnableWakeLock`, which requires the - `android.Manifest.permission#WAKE_LOCK` permission - ([#5846](https://github.com/google/ExoPlayer/issues/5846)). +* HLS: Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* AV1 extension: + * New in this release. The AV1 extension allows use of the + [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) + in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer + [here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9). * VP9 extension: - * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` - and move it to the core library. + * Update to use NDK r20. + * Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the + core library. * Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. * Use `VideoDecoderRenderer` as an implementation of `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. -* Rename `spherical_view` surface type to `spherical_gl_surface_view`. -* Add automatic audio becoming noisy handling to `SimpleExoPlayer`, - available through `SimpleExoPlayer.setHandleAudioBecomingNoisy`. -* Post `AudioFocusManager.onAudioFocusChange` events to eventHandler, avoiding - multithreaded access to the player or audio focus manager. -* Add `Timeline.Window.isLive` to indicate that a window is a live stream - ([#2668](https://github.com/google/ExoPlayer/issues/2668) and - [#5973](https://github.com/google/ExoPlayer/issues/5973)). -* Fail more explicitly when local-file Uris contain invalid parts (e.g. - fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). -* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. -* Make show and hide player controls accessible for TalkBack in `PlayerView`. -* Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. -* Deprecate the GVR extension. -* Fix the start of audio getting truncated when transitioning to a new - item in a playlist of opus streams. -* Fix FLAC extension build - ([#6601](https://github.com/google/ExoPlayer/issues/6601). -* Update the ffmpeg, flac and opus extension build instructions to use NDK r20. -* Update the ffmpeg extension to release 4.2. It is necessary to rebuild the - native part of the extension after this change, following the instructions in - the extension's readme. -* Add support for subtitle files to the demo app - ([#5523](https://github.com/google/ExoPlayer/issues/5523)). -* Handle new signaling for E-AC3 JOC audio in DASH - ([#6636](https://github.com/google/ExoPlayer/issues/6636)). -* Require an end time or duration for SubRip (SRT) and SubStation Alpha - (SSA/ASS) subtitles. This applies to both sidecar files & subtitles - [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). +* Flac extension: + * Update to use NDK r20. + * Fix build + ([#6601](https://github.com/google/ExoPlayer/issues/6601). +* FFmpeg extension: + * Update to use NDK r20. + * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part + of the extension after this change, following the instructions in the + extension's readme. +* Opus extension: Update to use NDK r20. +* GVR extension: This extension is now deprecated. +* Demo apps (TODO: update links to point to r2.11.0 tag): + * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/surface) + to show how to use the Android 10 `SurfaceControl` API with ExoPlayer + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Add support for subtitle files to the + [Main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main) + ([#5523](https://github.com/google/ExoPlayer/issues/5523)). + * Remove the IMA demo app. IMA functionality is demonstrated by the + [main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main). + * Add basic DRM support to the + [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). +* TestUtils: Publish the `testutils` module to simplify unit testing with + ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). ### 2.10.7 (2019-11-12) ### @@ -121,7 +142,7 @@ * MediaSession extension: Update shuffle and repeat modes when playback state is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)). * Fix the start of audio getting truncated when transitioning to a new - item in a playlist of opus streams. + item in a playlist of Opus streams. ### 2.10.6 (2019-10-17) ### @@ -690,7 +711,7 @@ and `AnalyticsListener` callbacks ([#4361](https://github.com/google/ExoPlayer/issues/4361) and [#4615](https://github.com/google/ExoPlayer/issues/4615)). -* UI components: +* UI: * Add option to `PlayerView` to show buffering view when playWhenReady is false ([#4304](https://github.com/google/ExoPlayer/issues/4304)). * Allow any `Drawable` to be used as `PlayerView` default artwork. @@ -846,7 +867,7 @@ * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. * Add possibility to cancel `PlayerMessage`s. -* UI components: +* UI: * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed video frame or media artwork visible when the player is reset ([#2843](https://github.com/google/ExoPlayer/issues/2843)). @@ -896,7 +917,7 @@ * Support live stream clipping with `ClippingMediaSource`. * Allow setting tags for all media sources in their factories. The tag of the current window can be retrieved with `Player.getCurrentTag`. -* UI components: +* UI: * Add support for displaying error messages and a buffering spinner in `PlayerView`. * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update @@ -1060,7 +1081,7 @@ `SsMediaSource.Factory`, and `MergingMediaSource`. * Play out existing buffer before retrying for progressive live streams ([#1606](https://github.com/google/ExoPlayer/issues/1606)). -* UI components: +* UI: * Generalized player and control views to allow them to bind with any `Player`, and renamed them to `PlayerView` and `PlayerControlView` respectively. From 9b4a3701d49092209b4e86748bd3aa58acc4036f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Nov 2019 03:03:34 +0000 Subject: [PATCH 722/807] Clean up ExoMediaDrm reference counting documentation PiperOrigin-RevId: 280106092 --- .../android/exoplayer2/drm/ExoMediaDrm.java | 41 +++++++++++-------- .../exoplayer2/drm/FrameworkMediaDrm.java | 6 +-- 2 files changed, 27 insertions(+), 20 deletions(-) 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 1d0e15e81c..b6ee644842 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 @@ -31,6 +31,16 @@ import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. + * + *

        Reference counting

        + * + *

        Access to an instance is managed by reference counting, where {@link #acquire()} increments + * the reference count and {@link #release()} decrements it. When the reference count drops to 0 + * underlying resources are released, and the instance cannot be re-used. + * + *

        Each new instance has an initial reference count of 1. Hence application code that creates a + * new instance does not normally need to call {@link #acquire()}, and must call {@link #release()} + * when the instance is no longer required. */ public interface ExoMediaDrm { @@ -38,26 +48,25 @@ public interface ExoMediaDrm { interface Provider { /** - * Returns an {@link ExoMediaDrm} instance with acquired ownership for the DRM scheme identified - * by the given UUID. - * - *

        Each call to this method must have a corresponding call to {@link ExoMediaDrm#release()} - * to ensure correct resource management. + * Returns an {@link ExoMediaDrm} instance with an incremented reference count. When the caller + * no longer needs to use the instance, it must call {@link ExoMediaDrm#release()} to decrement + * the reference count. */ ExoMediaDrm acquireExoMediaDrm(UUID uuid); } /** - * {@link Provider} implementation which provides an {@link ExoMediaDrm} instance owned by the - * app. + * Provides an {@link ExoMediaDrm} instance owned by the app. * - *

        This provider should be used to manually handle {@link ExoMediaDrm} resources. + *

        Note that when using this provider the app will have instantiated the {@link ExoMediaDrm} + * instance, and remains responsible for calling {@link ExoMediaDrm#release()} on the instance + * when it's no longer being used. */ final class AppManagedProvider implements Provider { private final ExoMediaDrm exoMediaDrm; - /** Creates an instance, which provides the given {@link ExoMediaDrm}. */ + /** Creates an instance that provides the given {@link ExoMediaDrm}. */ public AppManagedProvider(ExoMediaDrm exoMediaDrm) { this.exoMediaDrm = exoMediaDrm; } @@ -269,17 +278,17 @@ public interface ExoMediaDrm { Map queryKeyStatus(byte[] sessionId); /** - * Acquires ownership over this instance, which must be released by calling {@link #release()}. + * Increments the reference count. When the caller no longer needs to use the instance, it must + * call {@link #release()} to decrement the reference count. + * + *

        A new instance will have an initial reference count of 1, and therefore it is not normally + * necessary for application code to call this method. */ void acquire(); /** - * Releases ownership of this instance. If a call to this method causes this instance to have no - * acquired ownerships, releases the underlying resources. - * - *

        Callers of this method must not make any further use of this instance. - * - * @see MediaDrm#release() + * Decrements the reference count. If the reference count drops to 0 underlying resources are + * released, and the instance cannot be re-used. */ void release(); 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 d040552638..8ac92b093c 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 @@ -55,8 +55,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrmThis provider should be used to make ExoPlayer handle {@link ExoMediaDrm} resources. */ public static final Provider DEFAULT_PROVIDER = uuid -> { @@ -78,8 +76,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Wed, 13 Nov 2019 07:41:04 +0000 Subject: [PATCH 723/807] Make DrmSession acquire/release consistent with ExoMediaDrm This aligns the method naming and Javadoc. The only remaining inconsistency I can see is that the initial reference count for DrmSession is 0 rather than 1. Unfortunately I think it's not trivial to get these aligned, because DefaultDrmSessionManager relies on being able to do something between instantiation and the DrmSession starting to open the session. In practice this doesn't really matter, since DrmSessions will be obtained via the manager, which does increment thee reference count to 1 to be consistent with how ExoMediaDrm acquisition works. PiperOrigin-RevId: 280136574 --- .../android/exoplayer2/BaseRenderer.java | 2 +- .../audio/SimpleDecoderAudioRenderer.java | 4 +-- .../exoplayer2/drm/DefaultDrmSession.java | 5 ++-- .../drm/DefaultDrmSessionManager.java | 4 +-- .../android/exoplayer2/drm/DrmSession.java | 26 +++++++++---------- .../exoplayer2/drm/DrmSessionManager.java | 12 ++++----- .../exoplayer2/drm/ErrorStateDrmSession.java | 4 +-- .../exoplayer2/drm/OfflineLicenseHelper.java | 4 +-- .../mediacodec/MediaCodecRenderer.java | 4 +-- .../source/SampleMetadataQueue.java | 4 +-- .../video/SimpleDecoderVideoRenderer.java | 4 +-- 11 files changed, 36 insertions(+), 37 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 6c323deca4..3cdab8baf1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -321,7 +321,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { Assertions.checkNotNull(Looper.myLooper()), newFormat.drmInitData); } if (existingSourceSession != null) { - existingSourceSession.releaseReference(); + existingSourceSession.release(); } return newSourceDrmSession; } 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 32516e7dcd..76abfd6e0f 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 @@ -657,12 +657,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(sourceDrmSession, session); + DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; } private void setDecoderDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(decoderDrmSession, session); + DrmSession.replaceSession(decoderDrmSession, session); decoderDrmSession = session; } 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 d962efd96b..25a08c9058 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 @@ -251,7 +251,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public void acquireReference() { + public void acquire() { + Assertions.checkState(referenceCount >= 0); if (++referenceCount == 1) { Assertions.checkState(state == STATE_OPENING); requestHandlerThread = new HandlerThread("DrmRequestHandler"); @@ -264,7 +265,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public void releaseReference() { + public void release() { if (--referenceCount == 0) { // Assigning null to various non-null variables for clean-up. state = STATE_RELEASED; 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 15f87318f0..a65f45309a 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 @@ -480,7 +480,7 @@ public class DefaultDrmSessionManager implements DrmSe sessions.add(placeholderDrmSession); this.placeholderDrmSession = placeholderDrmSession; } - placeholderDrmSession.acquireReference(); + placeholderDrmSession.acquire(); return placeholderDrmSession; } @@ -521,7 +521,7 @@ public class DefaultDrmSessionManager implements DrmSe } sessions.add(session); } - session.acquireReference(); + session.acquire(); return session; } 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 722ab946f0..13e29e141a 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 @@ -30,21 +30,21 @@ import java.util.Map; public interface DrmSession { /** - * 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. + * Invokes {@code newSession's} {@link #acquire()} and {@code previousSession's} {@link + * #release()} in that order. Null arguments are ignored. Does nothing if {@code previousSession} + * and {@code newSession} are the same session. */ - static void replaceSessionReferences( + static void replaceSession( @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { if (previousSession == newSession) { // Do nothing. return; } if (newSession != null) { - newSession.acquireReference(); + newSession.acquire(); } if (previousSession != null) { - previousSession.releaseReference(); + previousSession.release(); } } @@ -130,16 +130,14 @@ public interface DrmSession { byte[] getOfflineLicenseKeySetId(); /** - * Increments the reference count for this session. A non-zero reference count session will keep - * any acquired resources. + * Increments the reference count. When the caller no longer needs to use the instance, it must + * call {@link #release()} to decrement the reference count. */ - void acquireReference(); + void acquire(); /** - * 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. + * Decrements the reference count. If the reference count drops to 0 underlying resources are + * released, and the instance cannot be re-used. */ - void releaseReference(); + void release(); } 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 1a9e01441f..c92d68ed17 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 @@ -105,8 +105,9 @@ public interface DrmSessionManager { boolean canAcquireSession(DrmInitData drmInitData); /** - * Returns a {@link DrmSession} with an acquired reference that does not execute key requests. - * Returns null if placeholder sessions are not supported by this DRM session manager. + * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference + * count. When the caller no longer needs to use the instance, it must call {@link + * DrmSession#release()} to decrement the reference count. * *

        Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for * playback of clear samples, which reduces the costs of transitioning between clear and encrypted @@ -124,10 +125,9 @@ public interface DrmSessionManager { } /** - * 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. + * Returns a {@link DrmSession} for the specified {@link DrmInitData}, with an incremented + * reference count. When the caller no longer needs to use the instance, it must call {@link + * DrmSession#release()} to decrement the reference count. * * @param playbackLooper The looper associated with the media playback thread. * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain 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 d40cf60906..aa15c82900 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 @@ -58,12 +58,12 @@ public final class ErrorStateDrmSession implements Drm } @Override - public void acquireReference() { + public void acquire() { // Do nothing. } @Override - public void releaseReference() { + public void release() { // Do nothing. } } 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 9ed2fe3f27..31211d7b2a 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 @@ -210,7 +210,7 @@ public final class OfflineLicenseHelper { DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSession.releaseReference(); + drmSession.release(); drmSessionManager.release(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { @@ -236,7 +236,7 @@ public final class OfflineLicenseHelper { drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSession.releaseReference(); + drmSession.release(); drmSessionManager.release(); if (error != null) { throw error; 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 8f8d600f93..80d2625a3e 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 @@ -1011,12 +1011,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(sourceDrmSession, session); + DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; } private void setCodecDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(codecDrmSession, session); + DrmSession.replaceSession(codecDrmSession, session); codecDrmSession = session; } 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 891f36f47c..0cc576a145 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 @@ -171,7 +171,7 @@ import java.io.IOException; /** Releases any owned {@link DrmSession} references. */ public void releaseDrmSessionReferences() { if (currentDrmSession != null) { - currentDrmSession.releaseReference(); + currentDrmSession.release(); currentDrmSession = null; // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData // != null implies currentSession != null @@ -629,7 +629,7 @@ import java.io.IOException; outputFormatHolder.drmSession = currentDrmSession; if (previousSession != null) { - previousSession.releaseReference(); + previousSession.release(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index c7dc056823..cd3823b342 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -635,12 +635,12 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // Internal methods. private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(sourceDrmSession, session); + DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; } private void setDecoderDrmSession(@Nullable DrmSession session) { - DrmSession.replaceSessionReferences(decoderDrmSession, session); + DrmSession.replaceSession(decoderDrmSession, session); decoderDrmSession = session; } From bee62948131d8f1fe92fae720c9b6f07aba97048 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Nov 2019 09:31:07 +0000 Subject: [PATCH 724/807] Add clear methods for VideoDecoderOutputBufferRenderer Also add some missing Nullable annotations. PiperOrigin-RevId: 280150512 --- .../com/google/android/exoplayer2/Player.java | 26 ++++++--- .../android/exoplayer2/SimpleExoPlayer.java | 53 +++++++++++++------ 2 files changed, 57 insertions(+), 22 deletions(-) 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 fa0caeb92d..a29851fefc 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 @@ -217,7 +217,7 @@ public interface Player { * * @param surface The surface to clear. */ - void clearVideoSurface(Surface surface); + void clearVideoSurface(@Nullable Surface surface); /** * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for @@ -240,7 +240,7 @@ public interface Player { * * @param surfaceHolder The surface holder. */ - void setVideoSurfaceHolder(SurfaceHolder surfaceHolder); + void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); /** * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being @@ -248,7 +248,7 @@ public interface Player { * * @param surfaceHolder The surface holder to clear. */ - void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder); + void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); /** * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the @@ -256,7 +256,7 @@ public interface Player { * * @param surfaceView The surface view. */ - void setVideoSurfaceView(SurfaceView surfaceView); + void setVideoSurfaceView(@Nullable SurfaceView surfaceView); /** * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one @@ -264,7 +264,7 @@ public interface Player { * * @param surfaceView The texture view to clear. */ - void clearVideoSurfaceView(SurfaceView surfaceView); + void clearVideoSurfaceView(@Nullable SurfaceView surfaceView); /** * Sets the {@link TextureView} onto which video will be rendered. The player will track the @@ -272,7 +272,7 @@ public interface Player { * * @param textureView The texture view. */ - void setVideoTextureView(TextureView textureView); + void setVideoTextureView(@Nullable TextureView textureView); /** * Clears the {@link TextureView} onto which video is being rendered if it matches the one @@ -280,7 +280,7 @@ public interface Player { * * @param textureView The texture view to clear. */ - void clearVideoTextureView(TextureView textureView); + void clearVideoTextureView(@Nullable TextureView textureView); /** * Sets the video decoder output buffer renderer. This is intended for use only with extension @@ -293,6 +293,18 @@ public interface Player { */ void setVideoDecoderOutputBufferRenderer( @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer); + + /** Clears the video decoder output buffer renderer. */ + void clearVideoDecoderOutputBufferRenderer(); + + /** + * Clears the video decoder output buffer renderer if it matches the one passed. Else does + * nothing. + * + * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer to clear. + */ + void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer); } /** The text component of a {@link Player}. */ 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 c0a45249e6..6bea81cd49 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 @@ -329,6 +329,7 @@ public class SimpleExoPlayer extends BasePlayer @Nullable private Format videoFormat; @Nullable private Format audioFormat; + @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer; @Nullable private Surface surface; private boolean ownsSurface; private @C.VideoScalingMode int videoScalingMode; @@ -520,7 +521,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void clearVideoSurface(Surface surface) { + public void clearVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); if (surface != null && surface == this.surface) { setVideoSurface(null); @@ -537,7 +538,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); removeSurfaceCallbacks(); this.surfaceHolder = surfaceHolder; @@ -559,7 +560,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { setVideoSurfaceHolder(null); @@ -567,17 +568,17 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void setVideoSurfaceView(SurfaceView surfaceView) { + public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } @Override - public void clearVideoSurfaceView(SurfaceView surfaceView) { + public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } @Override - public void setVideoTextureView(TextureView textureView) { + public void setVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThread(); removeSurfaceCallbacks(); this.textureView = textureView; @@ -602,7 +603,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void clearVideoTextureView(TextureView textureView) { + public void clearVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThread(); if (textureView != null && textureView == this.textureView) { setVideoTextureView(null); @@ -614,14 +615,22 @@ public class SimpleExoPlayer extends BasePlayer @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { verifyApplicationThread(); setVideoSurface(null); - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) - .setPayload(videoDecoderOutputBufferRenderer) - .send(); - } + setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer() { + verifyApplicationThread(); + setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null + && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) { + setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); } } @@ -1486,6 +1495,20 @@ public class SimpleExoPlayer extends BasePlayer this.ownsSurface = ownsSurface; } + private void setVideoDecoderOutputBufferRendererInternal( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) + .setPayload(videoDecoderOutputBufferRenderer) + .send(); + } + } + this.videoDecoderOutputBufferRenderer = videoDecoderOutputBufferRenderer; + } + private void maybeNotifySurfaceSizeChanged(int width, int height) { if (width != surfaceWidth || height != surfaceHeight) { surfaceWidth = width; From 0ff79c0e02282816bd457433ca3135650d793cbd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Nov 2019 10:05:46 +0000 Subject: [PATCH 725/807] Support switching between Surface and VideoDecoderOutputBufferRenderer Clear state for one mode when entering the other in both SimpleExoPlayer and SimpleDecoderVideoRenderer. The latter is redundant for the case of renderers that are used inside SimpleExoPlayer, but seems nice to have. - Entering Surface mode means receiving a non-null Surface, SurfaceHolder or TextureView in SimpleExoPlayer, or a non-null Surface in SimpleDecoderVideoRenderer. - Entering VideoDecoderOutputBufferRenderer means receiving a non-null VideoDecoderOutputBufferRenderer in SimpleExoPlayer and SimpleDecoderVideoRenderer. PiperOrigin-RevId: 280155151 --- .../android/exoplayer2/SimpleExoPlayer.java | 33 +++++++++++++------ .../video/SimpleDecoderVideoRenderer.java | 2 ++ 2 files changed, 25 insertions(+), 10 deletions(-) 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 6bea81cd49..f1d01a114f 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 @@ -517,14 +517,16 @@ public class SimpleExoPlayer extends BasePlayer @Override public void clearVideoSurface() { verifyApplicationThread(); - setVideoSurface(null); + removeSurfaceCallbacks(); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @Override public void clearVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); if (surface != null && surface == this.surface) { - setVideoSurface(null); + clearVideoSurface(); } } @@ -532,7 +534,10 @@ public class SimpleExoPlayer extends BasePlayer public void setVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); removeSurfaceCallbacks(); - setVideoSurfaceInternal(surface, false); + if (surface != null) { + clearVideoDecoderOutputBufferRenderer(); + } + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); } @@ -541,9 +546,12 @@ public class SimpleExoPlayer extends BasePlayer public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); removeSurfaceCallbacks(); + if (surfaceHolder != null) { + clearVideoDecoderOutputBufferRenderer(); + } this.surfaceHolder = surfaceHolder; if (surfaceHolder == null) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(null, /* ownsSurface= */ false); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { surfaceHolder.addCallback(componentListener); @@ -581,9 +589,12 @@ public class SimpleExoPlayer extends BasePlayer public void setVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThread(); removeSurfaceCallbacks(); + if (textureView != null) { + clearVideoDecoderOutputBufferRenderer(); + } this.textureView = textureView; if (textureView == null) { - setVideoSurfaceInternal(null, true); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { if (textureView.getSurfaceTextureListener() != null) { @@ -614,7 +625,9 @@ public class SimpleExoPlayer extends BasePlayer public void setVideoDecoderOutputBufferRenderer( @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { verifyApplicationThread(); - setVideoSurface(null); + if (videoDecoderOutputBufferRenderer != null) { + clearVideoSurface(); + } setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer); } @@ -630,7 +643,7 @@ public class SimpleExoPlayer extends BasePlayer verifyApplicationThread(); if (videoDecoderOutputBufferRenderer != null && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) { - setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); + clearVideoDecoderOutputBufferRenderer(); } } @@ -1729,7 +1742,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void surfaceDestroyed(SurfaceHolder holder) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -1737,7 +1750,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { - setVideoSurfaceInternal(new Surface(surfaceTexture), true); + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(width, height); } @@ -1748,7 +1761,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - setVideoSurfaceInternal(null, true); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index cd3823b342..86181664ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -580,6 +580,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // The output has changed. this.surface = surface; if (surface != null) { + outputBufferRenderer = null; outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV; if (decoder != null) { setDecoderOutputMode(outputMode); @@ -608,6 +609,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // The output has changed. this.outputBufferRenderer = outputBufferRenderer; if (outputBufferRenderer != null) { + surface = null; outputMode = C.VIDEO_OUTPUT_MODE_YUV; if (decoder != null) { setDecoderOutputMode(outputMode); From 65b49a49f7ab6f70b37eedfce7c6cc057441198f Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 13 Nov 2019 11:50:34 +0000 Subject: [PATCH 726/807] Fix parameter name mismatch in Playlist PiperOrigin-RevId: 280167223 --- .../src/main/java/com/google/android/exoplayer2/Playlist.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java index 351c9d5780..24ea65893e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java @@ -119,7 +119,7 @@ import java.util.Set; MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1); Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); holder.reset( - /* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild + /* firstWindowIndexInChild= */ previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { holder.reset(/* firstWindowIndexInChild= */ 0); From 51711a0c97e6ed012356951591ef07135ef2a20b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 13 Nov 2019 13:08:08 +0000 Subject: [PATCH 727/807] Fix MediaDrm leaks in OfflineLicenseHelper PiperOrigin-RevId: 280176216 --- RELEASENOTES.md | 2 ++ .../exoplayer2/drm/OfflineLicenseHelper.java | 36 +++++++++++-------- .../drm/OfflineLicenseHelperTest.java | 6 +++- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b4485accbd..afdae31cf3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * Require an end time or duration for SubRip (SRT) and SubStation Alpha (SSA/ASS) subtitles. This applies to both sidecar files & subtitles [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). +* Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid `ExoMediaDrm` + leaks ([#4721](https://github.com/google/ExoPlayer/issues/4721)). ### 2.11.0 (not yet released) ### 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 31211d7b2a..93a7585f89 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 @@ -29,7 +29,8 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; -import java.util.HashMap; +import java.util.Collections; +import java.util.Map; import java.util.UUID; /** Helper class to download, renew and release offline licenses. */ @@ -89,21 +90,21 @@ public final class OfflineLicenseHelper { * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that * include their own license URL. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link MediaDrm#getKeyRequest}. May be null. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. - * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap) + * @see DefaultDrmSessionManager.Builder */ public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, - @Nullable HashMap optionalKeyRequestParameters) + @Nullable Map optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(C.WIDEVINE_UUID, - FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + return new OfflineLicenseHelper<>( + C.WIDEVINE_UUID, + FrameworkMediaDrm.DEFAULT_PROVIDER, new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } @@ -112,18 +113,18 @@ public final class OfflineLicenseHelper { * Constructs an instance. Call {@link #release()} when the instance is no longer required. * * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param mediaDrmProvider A {@link ExoMediaDrm.Provider}. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap) + * to {@link MediaDrm#getKeyRequest}. May be null. + * @see DefaultDrmSessionManager.Builder */ + @SuppressWarnings("unchecked") public OfflineLicenseHelper( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm.Provider mediaDrmProvider, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters) { + @Nullable Map optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); conditionVariable = new ConditionVariable(); @@ -149,8 +150,15 @@ public final class OfflineLicenseHelper { conditionVariable.open(); } }; + if (optionalKeyRequestParameters == null) { + optionalKeyRequestParameters = Collections.emptyMap(); + } drmSessionManager = - new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters); + (DefaultDrmSessionManager) + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider) + .setKeyRequestParameters(optionalKeyRequestParameters) + .build(callback); drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener); } 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 c371389483..e3dd679b92 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 @@ -51,7 +51,11 @@ public class OfflineLicenseHelperTest { .thenReturn( new ExoMediaDrm.KeyRequest(/* data= */ new byte[0], /* licenseServerUrl= */ "")); offlineLicenseHelper = - new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); + new OfflineLicenseHelper<>( + C.WIDEVINE_UUID, + new ExoMediaDrm.AppManagedProvider<>(mediaDrm), + mediaDrmCallback, + null); } @After From c8e7ecd36794e4030ff1d0a56d25e5e0e6e0a216 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Nov 2019 14:03:59 +0000 Subject: [PATCH 728/807] Merge consecutive segments for downloading. This speeds up downloads where segments have the same URL with different byte ranges. We limit the merged segments to 20 seconds to ensure the download progress of demuxed streams is roughly in line with the playable media duration. Issue:#5978 PiperOrigin-RevId: 280410761 --- RELEASENOTES.md | 2 + .../exoplayer2/offline/SegmentDownloader.java | 44 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index afdae31cf3..969b549083 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -137,6 +137,8 @@ [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* Downloads: Merge downloads in `SegmentDownloader` to improve overall download + speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). ### 2.10.7 (2019-11-12) ### 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 969003101f..5155685999 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 @@ -25,11 +25,13 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -62,6 +64,7 @@ public abstract class SegmentDownloader> impleme } private static final int BUFFER_SIZE_BYTES = 128 * 1024; + private static final long MAX_MERGED_SEGMENT_START_TIME_DIFF_US = 20 * C.MICROS_PER_SECOND; private final DataSpec manifestDataSpec; private final Cache cache; @@ -108,6 +111,8 @@ public abstract class SegmentDownloader> impleme manifest = manifest.copy(streamKeys); } List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); + Collections.sort(segments); + mergeSegments(segments, cacheKeyFactory); // Scan the segments, removing any that are fully downloaded. int totalSegments = segments.size(); @@ -134,7 +139,6 @@ public abstract class SegmentDownloader> impleme contentLength = C.LENGTH_UNSET; } } - Collections.sort(segments); // Download the segments. @Nullable ProgressNotifier progressNotifier = null; @@ -232,6 +236,44 @@ public abstract class SegmentDownloader> impleme /* flags= */ DataSpec.FLAG_ALLOW_GZIP); } + private static void mergeSegments(List segments, CacheKeyFactory keyFactory) { + HashMap lastIndexByCacheKey = new HashMap<>(); + int nextOutIndex = 0; + for (int i = 0; i < segments.size(); i++) { + Segment segment = segments.get(i); + String cacheKey = keyFactory.buildCacheKey(segment.dataSpec); + @Nullable Integer lastIndex = lastIndexByCacheKey.get(cacheKey); + @Nullable Segment lastSegment = lastIndex == null ? null : segments.get(lastIndex); + if (lastSegment == null + || segment.startTimeUs > lastSegment.startTimeUs + MAX_MERGED_SEGMENT_START_TIME_DIFF_US + || !canMergeSegments(lastSegment.dataSpec, segment.dataSpec)) { + lastIndexByCacheKey.put(cacheKey, nextOutIndex); + segments.set(nextOutIndex, segment); + nextOutIndex++; + } else { + long mergedLength = + segment.dataSpec.length == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : lastSegment.dataSpec.length + segment.dataSpec.length; + DataSpec mergedDataSpec = lastSegment.dataSpec.subrange(/* offset= */ 0, mergedLength); + segments.set( + Assertions.checkNotNull(lastIndex), + new Segment(lastSegment.startTimeUs, mergedDataSpec)); + } + } + Util.removeRange(segments, /* fromIndex= */ nextOutIndex, /* toIndex= */ segments.size()); + } + + private static boolean canMergeSegments(DataSpec dataSpec1, DataSpec dataSpec2) { + return dataSpec1.uri.equals(dataSpec2.uri) + && dataSpec1.length != C.LENGTH_UNSET + && (dataSpec1.absoluteStreamPosition + dataSpec1.length == dataSpec2.absoluteStreamPosition) + && Util.areEqual(dataSpec1.key, dataSpec2.key) + && dataSpec1.flags == dataSpec2.flags + && dataSpec1.httpMethod == dataSpec2.httpMethod + && dataSpec1.httpRequestHeaders.equals(dataSpec2.httpRequestHeaders); + } + private static final class ProgressNotifier implements CacheUtil.ProgressListener { private final ProgressListener progressListener; From 5579cc89c6e5d75d2c23513e67532e54118d26af Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 14 Nov 2019 18:18:02 +0000 Subject: [PATCH 729/807] Propagate end of stream received from `OnFrameRenderedListener` Previously the renderer EOS (aka last frame rendered), was reported as soon as the last encoded frame was queued in the codec renderer. This leaded to EOS reported too early. PiperOrigin-RevId: 280456277 --- RELEASENOTES.md | 3 +++ .../mediacodec/MediaCodecRenderer.java | 14 ++++++++++++++ .../video/MediaCodecVideoRenderer.java | 19 +++++++++++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 969b549083..a898646b30 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### dev-v2 (not yet released) ### +* Video tunneling: Fix renderer end-of-stream with `OnFrameRenderedListener` + from API 23, tunneled renderer must send a special timestamp on EOS. + Previously the EOS was reported when the input stream reached EOS. * Require an end time or duration for SubRip (SRT) and SubStation Alpha (SSA/ASS) subtitles. This applies to both sidecar files & subtitles [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). 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 80d2625a3e..54e0904dab 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 @@ -373,6 +373,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; private boolean skipMediaCodecStopOnRelease; + private boolean pendingOutputEndOfStream; protected DecoderCounters decoderCounters; @@ -603,6 +604,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; + pendingOutputEndOfStream = false; flushOrReinitializeCodec(); formatQueue.clear(); } @@ -686,6 +688,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (pendingOutputEndOfStream) { + pendingOutputEndOfStream = false; + processEndOfStream(); + } try { if (outputStreamEnded) { renderToEndOfStream(); @@ -1693,6 +1699,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + /** + * Notifies the renderer that output end of stream is pending and should be handled on the next + * render. + */ + protected final void setPendingOutputEndOfStream() { + pendingOutputEndOfStream = true; + } + private void reinitializeCodec() throws ExoPlaybackException { releaseCodec(); maybeInitCodec(); 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 871ad1689c..c04230e2b0 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 @@ -93,6 +93,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { */ private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + /** Magic frame render timestamp that indicates the EOS in tunneling mode. */ + private static final long TUNNELING_EOS_PRESENTATION_TIME_US = Long.MAX_VALUE; + /** A {@link DecoderException} with additional surface information. */ public static final class VideoDecoderException extends DecoderException { @@ -604,8 +607,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected boolean getCodecNeedsEosPropagation() { - // In tunneling mode we can't dequeue an end-of-stream buffer, so propagate it in the renderer. - return tunneling; + // Since API 23, onFrameRenderedListener allows for detection of the renderer EOS. + return tunneling && Util.SDK_INT < 23; } @Override @@ -946,6 +949,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { onProcessedOutputBuffer(presentationTimeUs); } + /** Called when a output EOS was received in tunneling mode. */ + private void onProcessedTunneledEndOfStream() { + setPendingOutputEndOfStream(); + } + /** * Called when an output buffer is successfully processed. * @@ -1754,9 +1762,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // Stale event. return; } - onProcessedTunneledBuffer(presentationTimeUs); + if (presentationTimeUs == TUNNELING_EOS_PRESENTATION_TIME_US) { + onProcessedTunneledEndOfStream(); + } else { + onProcessedTunneledBuffer(presentationTimeUs); + } } } - } From c0d393081675da622ee67b6cc469558ea1dfd257 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Nov 2019 04:29:30 +0000 Subject: [PATCH 730/807] Parse channel count and sample rate from ALAC initialization data Also remove the "do we really need to do this" comment for AAC. Parsing from codec specific data is likely to be more robust, so I think we should continue to do it for formats where we've seen this problem. Issue: #6648 PiperOrigin-RevId: 280575466 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 10 +++- .../util/CodecSpecificDataUtil.java | 34 +++++++++---- .../util/CodecSpecificDataUtilTest.java | 48 +++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java 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 e06a3278f0..bf05424b7f 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 @@ -1128,8 +1128,8 @@ import java.util.List; mimeType = mimeTypeAndInitializationData.first; initializationData = mimeTypeAndInitializationData.second; if (MimeTypes.AUDIO_AAC.equals(mimeType)) { - // TODO: Do we really need to do this? See [Internal: b/10903778] - // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See [Internal: b/10903778]. Pair audioSpecificConfig = CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); sampleRate = audioSpecificConfig.first; @@ -1174,6 +1174,12 @@ import java.util.List; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize); + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629. + Pair audioSpecificConfig = + CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; } childPosition += childAtomSize; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index f4f143f3b0..3372f23971 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -83,7 +83,7 @@ public final class CodecSpecificDataUtil { private CodecSpecificDataUtil() {} /** - * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. @@ -95,7 +95,7 @@ public final class CodecSpecificDataUtil { } /** - * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The * position is advanced to the end of the AudioSpecificConfig. @@ -104,8 +104,8 @@ public final class CodecSpecificDataUtil { * @return A pair consisting of the sample rate in Hz and the channel count. * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray, - boolean forceReadToEnd) throws ParserException { + public static Pair parseAacAudioSpecificConfig( + ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -166,10 +166,10 @@ public final class CodecSpecificDataUtil { * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param sampleRate The sample rate in Hz. - * @param numChannels The number of channels. + * @param channelCount The channel count. * @return The AudioSpecificConfig. */ - public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChannels) { + public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int channelCount) { int sampleRateIndex = C.INDEX_UNSET; for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) { if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) { @@ -178,13 +178,13 @@ public final class CodecSpecificDataUtil { } int channelConfig = C.INDEX_UNSET; for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) { - if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { + if (channelCount == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { channelConfig = i; } } if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) { - throw new IllegalArgumentException("Invalid sample rate or number of channels: " - + sampleRate + ", " + numChannels); + throw new IllegalArgumentException( + "Invalid sample rate or number of channels: " + sampleRate + ", " + channelCount); } return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig); } @@ -205,6 +205,22 @@ public final class CodecSpecificDataUtil { return specificConfig; } + /** + * Parses an ALAC AudioSpecificConfig (i.e. an ALACSpecificConfig). + * + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. + * @return A pair consisting of the sample rate in Hz and the channel count. + */ + public static Pair parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) { + ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig); + byteArray.setPosition(9); + int channelCount = byteArray.readUnsignedByte(); + byteArray.setPosition(20); + int sampleRate = byteArray.readUnsignedIntToInt(); + return Pair.create(sampleRate, channelCount); + } + /** * Builds an RFC 6381 AVC codec string using the provided parameters. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java new file mode 100644 index 0000000000..a880e82250 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/CodecSpecificDataUtilTest.java @@ -0,0 +1,48 @@ +/* + * 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 android.util.Pair; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link CodecSpecificDataUtil}. */ +@RunWith(AndroidJUnit4.class) +public class CodecSpecificDataUtilTest { + + @Test + public void parseAlacAudioSpecificConfig() { + byte[] alacSpecificConfig = + new byte[] { + 0, 0, 16, 0, // frameLength + 0, // compatibleVersion + 16, // bitDepth + 40, 10, 14, // tuning parameters + 2, // numChannels = 2 + 0, 0, // maxRun + 0, 0, 64, 4, // maxFrameBytes + 0, 46, -32, 0, // avgBitRate + 0, 1, 119, 0, // sampleRate = 96000 + }; + Pair sampleRateAndChannelCount = + CodecSpecificDataUtil.parseAlacAudioSpecificConfig(alacSpecificConfig); + assertThat(sampleRateAndChannelCount.first).isEqualTo(96000); + assertThat(sampleRateAndChannelCount.second).isEqualTo(2); + } +} From 79b7af656bbc91c25570eef5aa8a7f72e3113ecb Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 15 Nov 2019 16:44:32 +0000 Subject: [PATCH 731/807] Rollback of *** Original commit *** Disable test coverage again https://issuetracker.google.com/issues/37019591 causes local variables can't be found while debugging. *** PiperOrigin-RevId: 280666758 --- library/core/build.gradle | 8 +++----- library/dash/build.gradle | 8 +++----- library/hls/build.gradle | 8 +++----- library/smoothstreaming/build.gradle | 8 +++----- library/ui/build.gradle | 8 +++----- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index 32beddfd89..e9cb6d5a18 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -46,11 +46,9 @@ android { } buildTypes { - // Re-enable test coverage when the following issue is fixed: - // https://issuetracker.google.com/issues/37019591 - // debug { - // testCoverageEnabled = true - // } + debug { + testCoverageEnabled = true + } } testOptions.unitTests.includeAndroidResources = true diff --git a/library/dash/build.gradle b/library/dash/build.gradle index ac90d64c1e..f51fc509cc 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -28,11 +28,9 @@ android { } buildTypes { - // Re-enable test coverage when the following issue is fixed: - // https://issuetracker.google.com/issues/37019591 - // debug { - // testCoverageEnabled = true - // } + debug { + testCoverageEnabled = true + } } testOptions.unitTests.includeAndroidResources = true diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 07696c1c26..df880f7f41 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -28,11 +28,9 @@ android { } buildTypes { - // Re-enable test coverage when the following issue is fixed: - // https://issuetracker.google.com/issues/37019591 - // debug { - // testCoverageEnabled = true - // } + debug { + testCoverageEnabled = true + } } testOptions.unitTests.includeAndroidResources = true diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 4fe2fae328..6ced528631 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -28,11 +28,9 @@ android { } buildTypes { - // Re-enable test coverage when the following issue is fixed: - // https://issuetracker.google.com/issues/37019591 - // debug { - // testCoverageEnabled = true - // } + debug { + testCoverageEnabled = true + } } testOptions.unitTests.includeAndroidResources = true diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 509dd22a28..b6bf139963 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -28,11 +28,9 @@ android { } buildTypes { - // Re-enable test coverage when the following issue is fixed: - // https://issuetracker.google.com/issues/37019591 - // debug { - // testCoverageEnabled = true - // } + debug { + testCoverageEnabled = true + } } testOptions.unitTests.includeAndroidResources = true From cedada09883b32aec8614854a3faed941daa2a30 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 17 Nov 2019 05:07:57 +0000 Subject: [PATCH 732/807] Revert "Merge consecutive segments for downloading." This reverts commit c8e7ecd36794e4030ff1d0a56d25e5e0e6e0a216. --- RELEASENOTES.md | 2 - .../exoplayer2/offline/SegmentDownloader.java | 44 +------------------ 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a898646b30..5e4c9b78aa 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -140,8 +140,6 @@ [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). -* Downloads: Merge downloads in `SegmentDownloader` to improve overall download - speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). ### 2.10.7 (2019-11-12) ### 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 5155685999..969003101f 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 @@ -25,13 +25,11 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,7 +62,6 @@ public abstract class SegmentDownloader> impleme } private static final int BUFFER_SIZE_BYTES = 128 * 1024; - private static final long MAX_MERGED_SEGMENT_START_TIME_DIFF_US = 20 * C.MICROS_PER_SECOND; private final DataSpec manifestDataSpec; private final Cache cache; @@ -111,8 +108,6 @@ public abstract class SegmentDownloader> impleme manifest = manifest.copy(streamKeys); } List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); - Collections.sort(segments); - mergeSegments(segments, cacheKeyFactory); // Scan the segments, removing any that are fully downloaded. int totalSegments = segments.size(); @@ -139,6 +134,7 @@ public abstract class SegmentDownloader> impleme contentLength = C.LENGTH_UNSET; } } + Collections.sort(segments); // Download the segments. @Nullable ProgressNotifier progressNotifier = null; @@ -236,44 +232,6 @@ public abstract class SegmentDownloader> impleme /* flags= */ DataSpec.FLAG_ALLOW_GZIP); } - private static void mergeSegments(List segments, CacheKeyFactory keyFactory) { - HashMap lastIndexByCacheKey = new HashMap<>(); - int nextOutIndex = 0; - for (int i = 0; i < segments.size(); i++) { - Segment segment = segments.get(i); - String cacheKey = keyFactory.buildCacheKey(segment.dataSpec); - @Nullable Integer lastIndex = lastIndexByCacheKey.get(cacheKey); - @Nullable Segment lastSegment = lastIndex == null ? null : segments.get(lastIndex); - if (lastSegment == null - || segment.startTimeUs > lastSegment.startTimeUs + MAX_MERGED_SEGMENT_START_TIME_DIFF_US - || !canMergeSegments(lastSegment.dataSpec, segment.dataSpec)) { - lastIndexByCacheKey.put(cacheKey, nextOutIndex); - segments.set(nextOutIndex, segment); - nextOutIndex++; - } else { - long mergedLength = - segment.dataSpec.length == C.LENGTH_UNSET - ? C.LENGTH_UNSET - : lastSegment.dataSpec.length + segment.dataSpec.length; - DataSpec mergedDataSpec = lastSegment.dataSpec.subrange(/* offset= */ 0, mergedLength); - segments.set( - Assertions.checkNotNull(lastIndex), - new Segment(lastSegment.startTimeUs, mergedDataSpec)); - } - } - Util.removeRange(segments, /* fromIndex= */ nextOutIndex, /* toIndex= */ segments.size()); - } - - private static boolean canMergeSegments(DataSpec dataSpec1, DataSpec dataSpec2) { - return dataSpec1.uri.equals(dataSpec2.uri) - && dataSpec1.length != C.LENGTH_UNSET - && (dataSpec1.absoluteStreamPosition + dataSpec1.length == dataSpec2.absoluteStreamPosition) - && Util.areEqual(dataSpec1.key, dataSpec2.key) - && dataSpec1.flags == dataSpec2.flags - && dataSpec1.httpMethod == dataSpec2.httpMethod - && dataSpec1.httpRequestHeaders.equals(dataSpec2.httpRequestHeaders); - } - private static final class ProgressNotifier implements CacheUtil.ProgressListener { private final ProgressListener progressListener; From a91ffdd4d136ef693266d76909afdc6e3398a5eb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 17 Nov 2019 05:08:24 +0000 Subject: [PATCH 733/807] Revert "Fix parameter name mismatch in Playlist" This reverts commit 65b49a49f7ab6f70b37eedfce7c6cc057441198f. --- .../src/main/java/com/google/android/exoplayer2/Playlist.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java index 24ea65893e..351c9d5780 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java @@ -119,7 +119,7 @@ import java.util.Set; MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1); Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); holder.reset( - /* firstWindowIndexInChild= */ previousHolder.firstWindowIndexInChild + /* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { holder.reset(/* firstWindowIndexInChild= */ 0); From 71dd3fe54e549032d4c84247b73a4eee431ac774 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 18 Nov 2019 05:37:44 +0000 Subject: [PATCH 734/807] Revert "Add a parameter object for LoadErrorHandlingPolicy methods" This reverts commit b84a9bed2caade60a06ac5b91bd66a5f3af6449d. --- .../upstream/LoadErrorHandlingPolicy.java | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java index 68ab2a7a47..293d1e7510 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -38,30 +38,6 @@ import java.io.IOException; */ public interface LoadErrorHandlingPolicy { - /** Holds information about a load task error. */ - final class LoadErrorInfo { - - /** One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to load. */ - public final int dataType; - /** - * The duration in milliseconds of the load from the start of the first load attempt up to the - * point at which the error occurred. - */ - public final long loadDurationMs; - /** The exception associated to the load error. */ - public final IOException exception; - /** The number of errors this load task has encountered, including this one. */ - public final int errorCount; - - /** Creates an instance with the given values. */ - public LoadErrorInfo(int dataType, long loadDurationMs, IOException exception, int errorCount) { - this.dataType = dataType; - this.loadDurationMs = loadDurationMs; - this.exception = exception; - this.errorCount = errorCount; - } - } - /** * Returns the number of milliseconds for which a resource associated to a provided load error * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. @@ -78,22 +54,6 @@ public interface LoadErrorHandlingPolicy { long getBlacklistDurationMsFor( int dataType, long loadDurationMs, IOException exception, int errorCount); - /** - * Returns the number of milliseconds for which a resource associated to a provided load error - * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. - * - * @param loadErrorInfo A {@link LoadErrorInfo} holding information about the load error. - * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should - * not be blacklisted. - */ - default long getBlacklistDurationMsFor(LoadErrorInfo loadErrorInfo) { - return getBlacklistDurationMsFor( - loadErrorInfo.dataType, - loadErrorInfo.loadDurationMs, - loadErrorInfo.exception, - loadErrorInfo.errorCount); - } - /** * Returns the number of milliseconds to wait before attempting the load again, or {@link * C#TIME_UNSET} if the error is fatal and should not be retried. @@ -113,26 +73,6 @@ public interface LoadErrorHandlingPolicy { */ long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount); - /** - * Returns the number of milliseconds to wait before attempting the load again, or {@link - * C#TIME_UNSET} if the error is fatal and should not be retried. - * - *

        {@link Loader} clients may ignore the retry delay returned by this method in order to wait - * for a specific event before retrying. However, the load is retried if and only if this method - * does not return {@link C#TIME_UNSET}. - * - * @param loadErrorInfo A {@link LoadErrorInfo} holding information about the load error. - * @return The number of milliseconds to wait before attempting the load again, or {@link - * C#TIME_UNSET} if the error is fatal and should not be retried. - */ - default long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) { - return getRetryDelayMsFor( - loadErrorInfo.dataType, - loadErrorInfo.loadDurationMs, - loadErrorInfo.exception, - loadErrorInfo.errorCount); - } - /** * Returns the minimum number of times to retry a load in the case of a load error, before * propagating the error. From c9cc147fb92778730f59251d894bb5eb1435a524 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 18 Nov 2019 05:38:22 +0000 Subject: [PATCH 735/807] Revert "Playlist API: add Playlist and PlaylistTest" This reverts commit 5c2806eccabcdeb8817b1ccb20bffeb087259f42. --- .../google/android/exoplayer2/Playlist.java | 707 ------------------ .../AbstractConcatenatedTimeline.java | 8 +- .../source/ConcatenatingMediaSource.java | 1 - .../exoplayer2/source/LoopingMediaSource.java | 1 - .../android/exoplayer2/PlaylistTest.java | 510 ------------- 5 files changed, 5 insertions(+), 1222 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/Playlist.java rename library/core/src/main/java/com/google/android/exoplayer2/{ => source}/AbstractConcatenatedTimeline.java (98%) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java b/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java deleted file mode 100644 index 351c9d5780..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/Playlist.java +++ /dev/null @@ -1,707 +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; - -import android.os.Handler; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; -import com.google.android.exoplayer2.source.MaskingMediaPeriod; -import com.google.android.exoplayer2.source.MaskingMediaSource; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -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; - -/** - * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified - * during playback. It is valid for the same {@link MediaSource} instance to be present more than - * once in the playlist. - * - *

        With the exception of the constructor, all methods are called on the playback thread. - */ -/* package */ class Playlist { - - /** Listener for source events. */ - public interface PlaylistInfoRefreshListener { - - /** - * Called when the timeline of a media item has changed and a new timeline that reflects the - * current playlist state needs to be created by calling {@link #createTimeline()}. - * - *

        Called on the playback thread. - */ - void onPlaylistUpdateRequested(); - } - - private final List mediaSourceHolders; - private final Map mediaSourceByMediaPeriod; - private final Map mediaSourceByUid; - private final PlaylistInfoRefreshListener playlistInfoListener; - private final MediaSourceEventListener.EventDispatcher eventDispatcher; - private final HashMap childSources; - private final Set enabledMediaSourceHolders; - - private ShuffleOrder shuffleOrder; - private boolean isPrepared; - - @Nullable private TransferListener mediaTransferListener; - - @SuppressWarnings("initialization") - public Playlist(PlaylistInfoRefreshListener listener) { - playlistInfoListener = listener; - shuffleOrder = new DefaultShuffleOrder(0); - mediaSourceByMediaPeriod = new IdentityHashMap<>(); - mediaSourceByUid = new HashMap<>(); - mediaSourceHolders = new ArrayList<>(); - eventDispatcher = new MediaSourceEventListener.EventDispatcher(); - childSources = new HashMap<>(); - enabledMediaSourceHolders = new HashSet<>(); - } - - /** - * Sets the media sources replacing any sources previously contained in the playlist. - * - * @param holders The list of {@link MediaSourceHolder}s to set. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - */ - public final Timeline setMediaSources( - List holders, ShuffleOrder shuffleOrder) { - removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); - return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder); - } - - /** - * Adds multiple {@link MediaSourceHolder}s to the playlist. - * - * @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index - * must be in the range of 0 <= index <= {@link #getSize()}. - * @param holders A list of {@link MediaSourceHolder}s to be added. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - */ - public final Timeline addMediaSources( - int index, List holders, ShuffleOrder shuffleOrder) { - if (!holders.isEmpty()) { - this.shuffleOrder = shuffleOrder; - for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) { - MediaSourceHolder holder = holders.get(insertionIndex - index); - if (insertionIndex > 0) { - MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1); - Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); - holder.reset( - /* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild - + previousTimeline.getWindowCount()); - } else { - holder.reset(/* firstWindowIndexInChild= */ 0); - } - Timeline newTimeline = holder.mediaSource.getTimeline(); - correctOffsets( - /* startIndex= */ insertionIndex, - /* windowOffsetUpdate= */ newTimeline.getWindowCount()); - mediaSourceHolders.add(insertionIndex, holder); - mediaSourceByUid.put(holder.uid, holder); - if (isPrepared) { - prepareChildSource(holder); - if (mediaSourceByMediaPeriod.isEmpty()) { - enabledMediaSourceHolders.add(holder); - } else { - disableChildSource(holder); - } - } - } - } - return createTimeline(); - } - - /** - * Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index - * (included) and a final index (excluded). - * - *

        Note: when specified range is empty, no actual media source is removed and no exception is - * thrown. - * - * @param fromIndex The initial range index, pointing to the first media source that will be - * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param toIndex The final range index, pointing to the first media source that will be left - * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, - * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} - */ - public final Timeline removeMediaSourceRange( - int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { - Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize()); - this.shuffleOrder = shuffleOrder; - removeMediaSourcesInternal(fromIndex, toIndex); - return createTimeline(); - } - - /** - * Moves an existing media source within the playlist. - * - * @param currentIndex The current index of the media source in the playlist. This index must be - * in the range of 0 <= index < {@link #getSize()}. - * @param newIndex The target index of the media source in the playlist. This index must be in the - * range of 0 <= index < {@link #getSize()}. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0, - * {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0 - */ - public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) { - return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder); - } - - /** - * Moves a range of media sources within the playlist. - * - *

        Note: when specified range is empty or the from index equals the new from index, no actual - * media source is moved and no exception is thrown. - * - * @param fromIndex The initial range index, pointing to the first media source of the range that - * will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}. - * @param toIndex The final range index, pointing to the first media source that will be left - * untouched. This index must be larger or equals than {@code fromIndex}. - * @param newFromIndex The target index of the first media source of the range that will be moved. - * @param shuffleOrder The new shuffle order. - * @return The new {@link Timeline}. - * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, - * {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code - * newFromIndex} < 0 - */ - public Timeline moveMediaSourceRange( - int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { - Assertions.checkArgument( - fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0); - this.shuffleOrder = shuffleOrder; - if (fromIndex == toIndex || fromIndex == newFromIndex) { - return createTimeline(); - } - int startIndex = Math.min(fromIndex, newFromIndex); - int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1; - int endIndex = Math.max(newEndIndex, toIndex - 1); - int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; - moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); - for (int i = startIndex; i <= endIndex; i++) { - MediaSourceHolder holder = mediaSourceHolders.get(i); - holder.firstWindowIndexInChild = windowOffset; - windowOffset += holder.mediaSource.getTimeline().getWindowCount(); - } - return createTimeline(); - } - - /** Clears the playlist. */ - public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) { - this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear(); - removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize()); - return createTimeline(); - } - - /** Whether the playlist is prepared. */ - public final boolean isPrepared() { - return isPrepared; - } - - /** Returns the number of media sources in the playlist. */ - public final int getSize() { - return mediaSourceHolders.size(); - } - - /** - * Sets the {@link AnalyticsCollector}. - * - * @param handler The handler on which to call the collector. - * @param analyticsCollector The analytics collector. - */ - public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) { - eventDispatcher.addEventListener(handler, analyticsCollector); - } - - /** - * Sets a new shuffle order to use when shuffling the child media sources. - * - * @param shuffleOrder A {@link ShuffleOrder}. - */ - public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) { - int size = getSize(); - if (shuffleOrder.getLength() != size) { - shuffleOrder = - shuffleOrder - .cloneAndClear() - .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); - } - this.shuffleOrder = shuffleOrder; - return createTimeline(); - } - - /** Prepares the playlist. */ - public final void prepare(@Nullable TransferListener mediaTransferListener) { - Assertions.checkState(!isPrepared); - this.mediaTransferListener = mediaTransferListener; - for (int i = 0; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - prepareChildSource(mediaSourceHolder); - enabledMediaSourceHolders.add(mediaSourceHolder); - } - isPrepared = true; - } - - /** - * Returns a new {@link MediaPeriod} identified by {@code periodId}. - * - * @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. - * @return A new {@link MediaPeriod}. - */ - public MediaPeriod createPeriod( - MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) { - Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); - MediaSource.MediaPeriodId childMediaPeriodId = - id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); - MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); - enableMediaSource(holder); - holder.activeMediaPeriodIds.add(childMediaPeriodId); - MediaPeriod mediaPeriod = - holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); - mediaSourceByMediaPeriod.put(mediaPeriod, holder); - disableUnusedMediaSources(); - return mediaPeriod; - } - - /** - * Releases the period. - * - * @param mediaPeriod The period to release. - */ - public final void releasePeriod(MediaPeriod mediaPeriod) { - MediaSourceHolder holder = - Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); - holder.mediaSource.releasePeriod(mediaPeriod); - holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); - if (!mediaSourceByMediaPeriod.isEmpty()) { - disableUnusedMediaSources(); - } - maybeReleaseChildSource(holder); - } - - /** Releases the playlist. */ - public final void release() { - for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.caller); - childSource.mediaSource.removeEventListener(childSource.eventListener); - } - childSources.clear(); - enabledMediaSourceHolders.clear(); - isPrepared = false; - } - - /** Throws any pending error encountered while loading or refreshing. */ - public final void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - /** Creates a timeline reflecting the current state of the playlist. */ - public final Timeline createTimeline() { - if (mediaSourceHolders.isEmpty()) { - return Timeline.EMPTY; - } - int windowOffset = 0; - for (int i = 0; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - mediaSourceHolder.firstWindowIndexInChild = windowOffset; - windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount(); - } - return new PlaylistTimeline(mediaSourceHolders, shuffleOrder); - } - - // Internal methods. - - private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { - enabledMediaSourceHolders.add(mediaSourceHolder); - @Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder); - if (enabledChild != null) { - enabledChild.mediaSource.enable(enabledChild.caller); - } - } - - private void disableUnusedMediaSources() { - Iterator iterator = enabledMediaSourceHolders.iterator(); - while (iterator.hasNext()) { - MediaSourceHolder holder = iterator.next(); - if (holder.activeMediaPeriodIds.isEmpty()) { - disableChildSource(holder); - iterator.remove(); - } - } - } - - private void disableChildSource(MediaSourceHolder holder) { - @Nullable MediaSourceAndListener disabledChild = childSources.get(holder); - if (disabledChild != null) { - disabledChild.mediaSource.disable(disabledChild.caller); - } - } - - private void removeMediaSourcesInternal(int fromIndex, int toIndex) { - for (int index = toIndex - 1; index >= fromIndex; index--) { - MediaSourceHolder holder = mediaSourceHolders.remove(index); - mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.mediaSource.getTimeline(); - correctOffsets( - /* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount()); - holder.isRemoved = true; - if (isPrepared) { - maybeReleaseChildSource(holder); - } - } - } - - private void correctOffsets(int startIndex, int windowOffsetUpdate) { - for (int i = startIndex; i < mediaSourceHolders.size(); i++) { - MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i); - mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate; - } - } - - // Internal methods to manage child sources. - - @Nullable - private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( - MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) { - for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) { - // Ensure the reported media period id has the same window sequence number as the one created - // by this media source. Otherwise it does not belong to this child source. - if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber - == mediaPeriodId.windowSequenceNumber) { - Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); - return mediaPeriodId.copyWithPeriodUid(periodUid); - } - } - return null; - } - - private static int getWindowIndexForChildWindowIndex( - MediaSourceHolder mediaSourceHolder, int windowIndex) { - return windowIndex + mediaSourceHolder.firstWindowIndexInChild; - } - - private void prepareChildSource(MediaSourceHolder holder) { - MediaSource mediaSource = holder.mediaSource; - MediaSource.MediaSourceCaller caller = - (source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested(); - MediaSourceEventListener eventListener = new ForwardingEventListener(holder); - childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener)); - mediaSource.addEventListener(new Handler(), eventListener); - mediaSource.prepareSource(caller, mediaTransferListener); - } - - private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { - // Release if the source has been removed from the playlist and no periods are still active. - if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { - MediaSourceAndListener removedChild = - Assertions.checkNotNull(childSources.remove(mediaSourceHolder)); - removedChild.mediaSource.releaseSource(removedChild.caller); - removedChild.mediaSource.removeEventListener(removedChild.eventListener); - enabledMediaSourceHolders.remove(mediaSourceHolder); - } - } - - /** Return uid of media source holder from period uid of concatenated source. */ - private static Object getMediaSourceHolderUid(Object periodUid) { - return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); - } - - /** Return uid of child period from period uid of concatenated source. */ - private static Object getChildPeriodUid(Object periodUid) { - return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); - } - - private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { - return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid); - } - - /* package */ static void moveMediaSourceHolders( - List mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) { - MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex]; - for (int i = removedItems.length - 1; i >= 0; i--) { - removedItems[i] = mediaSourceHolders.remove(fromIndex + i); - } - mediaSourceHolders.addAll( - Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems)); - } - - /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder { - - public final MaskingMediaSource mediaSource; - public final Object uid; - public final List activeMediaPeriodIds; - - public int firstWindowIndexInChild; - public boolean isRemoved; - - public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { - this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); - this.activeMediaPeriodIds = new ArrayList<>(); - this.uid = new Object(); - } - - public void reset(int firstWindowIndexInChild) { - this.firstWindowIndexInChild = firstWindowIndexInChild; - this.isRemoved = false; - this.activeMediaPeriodIds.clear(); - } - } - - /** Timeline exposing concatenated timelines of playlist media sources. */ - /* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline { - - private final int windowCount; - private final int periodCount; - private final int[] firstPeriodInChildIndices; - private final int[] firstWindowInChildIndices; - private final Timeline[] timelines; - private final Object[] uids; - private final HashMap childIndexByUid; - - public PlaylistTimeline( - Collection mediaSourceHolders, ShuffleOrder shuffleOrder) { - super(/* isAtomic= */ false, shuffleOrder); - int childCount = mediaSourceHolders.size(); - firstPeriodInChildIndices = new int[childCount]; - firstWindowInChildIndices = new int[childCount]; - timelines = new Timeline[childCount]; - uids = new Object[childCount]; - childIndexByUid = new HashMap<>(); - int index = 0; - int windowCount = 0; - int periodCount = 0; - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - 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 - protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); - } - - @Override - protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); - } - - @Override - protected int getChildIndexByChildUid(Object childUid) { - Integer index = childIndexByUid.get(childUid); - return index == null ? C.INDEX_UNSET : index; - } - - @Override - protected Timeline getTimelineByChildIndex(int childIndex) { - return timelines[childIndex]; - } - - @Override - protected int getFirstPeriodIndexByChildIndex(int childIndex) { - return firstPeriodInChildIndices[childIndex]; - } - - @Override - protected int getFirstWindowIndexByChildIndex(int childIndex) { - return firstWindowInChildIndices[childIndex]; - } - - @Override - protected Object getChildUidByChildIndex(int childIndex) { - return uids[childIndex]; - } - - @Override - public int getWindowCount() { - return windowCount; - } - - @Override - public int getPeriodCount() { - return periodCount; - } - } - - private static final class MediaSourceAndListener { - - public final MediaSource mediaSource; - public final MediaSource.MediaSourceCaller caller; - public final MediaSourceEventListener eventListener; - - public MediaSourceAndListener( - MediaSource mediaSource, - MediaSource.MediaSourceCaller caller, - MediaSourceEventListener eventListener) { - this.mediaSource = mediaSource; - this.caller = caller; - this.eventListener = eventListener; - } - } - - private final class ForwardingEventListener implements MediaSourceEventListener { - - private final Playlist.MediaSourceHolder id; - private EventDispatcher eventDispatcher; - - public ForwardingEventListener(Playlist.MediaSourceHolder id) { - eventDispatcher = Playlist.this.eventDispatcher; - this.id = id; - } - - @Override - public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodCreated(); - } - } - - @Override - public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodReleased(); - } - } - - @Override - public void onLoadStarted( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadStarted(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadCompleted( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadCompleted(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadCanceled( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadCanceled(loadEventData, mediaLoadData); - } - } - - @Override - public void onLoadError( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventData, - MediaLoadData mediaLoadData, - IOException error, - boolean wasCanceled) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled); - } - } - - @Override - public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.readingStarted(); - } - } - - @Override - public void onUpstreamDiscarded( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.upstreamDiscarded(mediaLoadData); - } - } - - @Override - public void onDownstreamFormatChanged( - int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.downstreamFormatChanged(mediaLoadData); - } - } - - /** Updates the event dispatcher and returns whether the event should be dispatched. */ - private boolean maybeUpdateEventDispatcher( - int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { - @Nullable MediaSource.MediaPeriodId mediaPeriodId = null; - if (childMediaPeriodId != null) { - mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); - if (mediaPeriodId == null) { - // Media period not found. Ignore event. - return false; - } - } - int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); - if (eventDispatcher.windowIndex != windowIndex - || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { - eventDispatcher = - Playlist.this.eventDispatcher.withParameters( - windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L); - } - return true; - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java similarity index 98% rename from library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 73bb49ed40..29ef1faa80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2; +package com.google.android.exoplayer2.source; import android.util.Pair; -import com.google.android.exoplayer2.source.ShuffleOrder; +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. */ -public abstract class AbstractConcatenatedTimeline extends Timeline { +/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; private final ShuffleOrder shuffleOrder; 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 c1ab78a9bc..545b8f5155 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 @@ -19,7 +19,6 @@ import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; 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 8769a84d95..cedc6f911d 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.AbstractConcatenatedTimeline; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java deleted file mode 100644 index cc551db8ac..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/PlaylistTest.java +++ /dev/null @@ -1,510 +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; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.ShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeMediaSource; -import com.google.android.exoplayer2.testutil.FakeShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeTimeline; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link Playlist}. */ -@RunWith(AndroidJUnit4.class) -public class PlaylistTest { - - private static final int PLAYLIST_SIZE = 4; - - private Playlist playlist; - - @Before - public void setUp() { - playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class)); - } - - @Test - public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); - List fakeHolders = createFakeHolders(); - - Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); - assertNotSame(timeline, Timeline.EMPTY); - - // Remove all media sources. - timeline = - playlist.removeMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder); - assertSame(timeline, Timeline.EMPTY); - - timeline = playlist.setMediaSources(fakeHolders, shuffleOrder); - assertNotSame(timeline, Timeline.EMPTY); - // Clear. - timeline = playlist.clear(shuffleOrder); - assertSame(timeline, Timeline.EMPTY); - } - - @Test - public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.setMediaSources( - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); - // Verify prepare is called once on prepare. - verify(mockMediaSource1, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - playlist.prepare(/* mediaTransferListener= */ null); - assertThat(playlist.isPrepared()).isTrue(); - // Verify prepare is called once on prepare. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - playlist.release(); - playlist.prepare(/* mediaTransferListener= */ null); - // Verify prepare is called a second time on re-prepare. - verify(mockMediaSource1, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder); - - assertThat(timeline.getWindowCount()).isEqualTo(2); - assertThat(playlist.getSize()).isEqualTo(2); - - // Assert holder offsets have been set properly - for (int i = 0; i < mediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); - } - - // Set media items again. The second holder is re-used. - List moreMediaSources = - createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); - moreMediaSources.add(mediaSources.get(1)); - timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - assertThat(timeline.getWindowCount()).isEqualTo(2); - for (int i = 0; i < moreMediaSources.size(); i++) { - Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i); - } - // Expect removed holders and sources to be removed without releasing. - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(0).isRemoved).isTrue(); - // Expect re-used holder and source not to be removed. - verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(1).isRemoved).isFalse(); - } - - @Test - public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources(mediaSources, shuffleOrder); - - // Verify sources are prepared. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - // Set media items again. The second holder is re-used. - List moreMediaSources = - createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class)); - moreMediaSources.add(mediaSources.get(1)); - playlist.setMediaSources(moreMediaSources, shuffleOrder); - - // Expect removed holders and sources to be removed and released. - verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(0).isRemoved).isTrue(); - // Expect re-used holder and source not to be removed but released. - verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSources.get(1).isRemoved).isFalse(); - verify(mockMediaSource2, times(2)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List mediaSources = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2)); - - assertThat(playlist.getSize()).isEqualTo(2); - // Verify lazy initialization does not call prepare on sources. - verify(mockMediaSource1, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - - for (int i = 0; i < mediaSources.size(); i++) { - assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i); - assertThat(mediaSources.get(i).isRemoved).isFalse(); - } - - // Add for more sources in between. - List moreMediaSources = createFakeHolders(); - playlist.addMediaSources( - /* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3)); - - assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0); - assertThat(moreMediaSources.get(0).firstWindowIndexInChild).isEqualTo(1); - assertThat(moreMediaSources.get(3).firstWindowIndexInChild).isEqualTo(4); - assertThat(mediaSources.get(1).firstWindowIndexInChild).isEqualTo(5); - } - - @Test - public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - playlist.prepare(/* mediaTransferListener= */ null); - playlist.addMediaSources( - /* index= */ 0, - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2)); - - // Verify prepare is called on sources when added. - verify(mockMediaSource1, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - verify(mockMediaSource2, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - } - - @Test - public void testMoveMediaSources() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - List holders = createFakeHolders(); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - - assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder); - assertFirstWindowInChildIndices(holders, 3, 0, 1, 2); - playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); - assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( - /* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder); - assertFirstWindowInChildIndices(holders, 2, 3, 0, 1); - playlist.moveMediaSourceRange( - /* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder); - assertFirstWindowInChildIndices(holders, 0, 3, 1, 2); - playlist.moveMediaSourceRange( - /* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - - // No-ops. - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder); - assertDefaultFirstWindowInChildIndexOrder(holders); - } - - @Test - public void testRemoveMediaSources_whenUnprepared_expectNoRelease() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - MediaSource mockMediaSource3 = mock(MediaSource.class); - MediaSource mockMediaSource4 = mock(MediaSource.class); - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, - mockMediaSource1, - mockMediaSource2, - mockMediaSource3, - mockMediaSource4); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - Playlist.MediaSourceHolder removedHolder1 = holders.remove(1); - Playlist.MediaSourceHolder removedHolder2 = holders.remove(1); - - assertDefaultFirstWindowInChildIndexOrder(holders); - assertThat(removedHolder1.isRemoved).isTrue(); - assertThat(removedHolder2.isRemoved).isTrue(); - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource3, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - } - - @Test - public void testRemoveMediaSources_whenPrepared_expectRelease() { - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - MediaSource mockMediaSource3 = mock(MediaSource.class); - MediaSource mockMediaSource4 = mock(MediaSource.class); - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, - mockMediaSource1, - mockMediaSource2, - mockMediaSource3, - mockMediaSource4); - playlist.prepare(/* mediaTransferListener */ null); - playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder); - playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder); - - assertThat(playlist.getSize()).isEqualTo(2); - holders.remove(2); - holders.remove(1); - - assertDefaultFirstWindowInChildIndexOrder(holders); - verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource3, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - } - - @Test - public void testRelease_playlistUnprepared_expectSourcesNotReleased() { - MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - - playlist.setMediaSources( - Collections.singletonList(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - verify(mockMediaSource, times(0)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - } - - @Test - public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() { - MediaSource mockMediaSource = mock(MediaSource.class); - Playlist.MediaSourceHolder mediaSourceHolder = - new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false); - - playlist.prepare(/* mediaTransferListener= */ null); - playlist.setMediaSources( - Collections.singletonList(mediaSourceHolder), - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1)); - verify(mockMediaSource, times(1)) - .prepareSource( - any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull()); - playlist.release(); - verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class)); - assertThat(mediaSourceHolder.isRemoved).isFalse(); - } - - @Test - public void testClearPlaylist_expectSourcesReleasedAndRemoved() { - ShuffleOrder.DefaultShuffleOrder shuffleOrder = - new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4); - MediaSource mockMediaSource1 = mock(MediaSource.class); - MediaSource mockMediaSource2 = mock(MediaSource.class); - List holders = - createFakeHoldersWithSources( - /* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2); - playlist.setMediaSources(holders, shuffleOrder); - playlist.prepare(/* mediaTransferListener= */ null); - - Timeline timeline = playlist.clear(shuffleOrder); - assertThat(timeline.isEmpty()).isTrue(); - assertThat(holders.get(0).isRemoved).isTrue(); - assertThat(holders.get(1).isRemoved).isTrue(); - verify(mockMediaSource1, times(1)).releaseSource(any()); - verify(mockMediaSource2, times(1)).releaseSource(any()); - } - - @Test - public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() { - Timeline timeline = - playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() { - Timeline timeline = - playlist.addMediaSources( - /* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.moveMediaSource( - /* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.moveMediaSourceRange( - /* fromIndex= */ 0, - /* toIndex= */ 2, - /* newFromIndex= */ 2, - new FakeShuffleOrder(PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() { - ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE); - playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder); - Timeline timeline = - playlist.removeMediaSourceRange( - /* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2)); - assertTimelineUsesFakeShuffleOrder(timeline); - } - - @Test - public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() { - playlist.setMediaSources( - createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE)); - assertTimelineUsesFakeShuffleOrder( - playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE))); - } - - // Internal methods. - - private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) { - assertThat( - timeline.getNextWindowIndex( - /* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) - .isEqualTo(-1); - assertThat( - timeline.getPreviousWindowIndex( - /* windowIndex= */ timeline.getWindowCount() - 1, - Player.REPEAT_MODE_OFF, - /* shuffleModeEnabled= */ true)) - .isEqualTo(-1); - } - - private static void assertDefaultFirstWindowInChildIndexOrder( - List holders) { - int[] indices = new int[holders.size()]; - for (int i = 0; i < indices.length; i++) { - indices[i] = i; - } - assertFirstWindowInChildIndices(holders, indices); - } - - private static void assertFirstWindowInChildIndices( - List holders, int... firstWindowInChildIndices) { - assertThat(holders).hasSize(firstWindowInChildIndices.length); - for (int i = 0; i < holders.size(); i++) { - assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]); - } - } - - private static List createFakeHolders() { - MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1)); - List holders = new ArrayList<>(); - for (int i = 0; i < PLAYLIST_SIZE; i++) { - holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true)); - } - return holders; - } - - private static List createFakeHoldersWithSources( - boolean useLazyPreparation, MediaSource... sources) { - List holders = new ArrayList<>(); - for (MediaSource mediaSource : sources) { - holders.add( - new Playlist.MediaSourceHolder( - mediaSource, /* useLazyPreparation= */ useLazyPreparation)); - } - return holders; - } -} From 30ed83ecef6ec17827fd1e9c43c82c65624bf541 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 18 Nov 2019 05:41:20 +0000 Subject: [PATCH 736/807] Revert "Playlist API: Add setMediaItem() and prepare()" This reverts commit cd2c1f2f24c3a17ffa59f0c5ba9d17d55f141793. --- .../exoplayer2/demo/PlayerActivity.java | 3 +- .../google/android/exoplayer2/ExoPlayer.java | 53 ++--- .../android/exoplayer2/ExoPlayerImpl.java | 71 ++---- .../android/exoplayer2/SimpleExoPlayer.java | 53 +---- .../android/exoplayer2/ExoPlayerTest.java | 217 ------------------ .../testutil/ExoPlayerTestRunner.java | 3 +- .../exoplayer2/testutil/StubExoPlayer.java | 15 -- 7 files changed, 48 insertions(+), 367 deletions(-) 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 2de117e9d7..2f8d0045d3 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 @@ -394,8 +394,7 @@ public class PlayerActivity extends AppCompatActivity if (haveStartPosition) { player.seekTo(startWindow, startPosition); } - player.setMediaItem(mediaSource); - player.prepare(); + player.prepare(mediaSource, !haveStartPosition, false); updateButtonVisibility(); } 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 99089a2afc..7c8a454191 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 @@ -331,51 +331,26 @@ public interface ExoPlayer extends Player { */ void retry(); - /** Prepares the player. */ - void prepare(); - /** - * @deprecated Use {@code setMediaItem(mediaSource, C.TIME_UNSET)} and {@link #prepare()} instead. + * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code + * prepare(mediaSource, true, true)}. */ - @Deprecated void prepare(MediaSource mediaSource); - /** @deprecated Use {@link #setMediaItem(MediaSource, long)} and {@link #prepare()} instead. */ - @Deprecated + /** + * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback + * position the default position in the first {@link Timeline.Window}. + * + * @param mediaSource The {@link MediaSource} to play. + * @param resetPosition Whether the playback position should be reset to the default position in + * the first {@link Timeline.Window}. If false, playback will start from the position defined + * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. + * @param resetState Whether the timeline, manifest, tracks and track selections should be reset. + * Should be true unless the player is being prepared to play the same media as it was playing + * previously (e.g. if playback failed and is being retried). + */ void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); - /** - * Sets the specified {@link MediaSource}. - * - *

        Note: This is an intermediate implementation towards a larger change. Until then {@link - * #prepare()} has to be called immediately after calling this method. - * - * @param mediaItem The new {@link MediaSource}. - */ - void setMediaItem(MediaSource mediaItem); - - /** - * Sets the specified {@link MediaSource}. - * - *

        Note: This is an intermediate implementation towards a larger change. Until then {@link - * #prepare()} has to be called immediately after calling this method. - * - *

        This intermediate implementation calls {@code stop(true)} before seeking to avoid seeking in - * a media item that has been set previously. It is equivalent with calling - * - *

        
        -   *   if (!getCurrentTimeline().isEmpty()) {
        -   *     player.stop(true);
        -   *   }
        -   *   player.seekTo(0, startPositionMs);
        -   *   player.setMediaItem(mediaItem);
        -   * 
        - * - * @param mediaItem The new {@link MediaSource}. - * @param startPositionMs The position in milliseconds to start playback from. - */ - void setMediaItem(MediaSource mediaItem, long startPositionMs); - /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * will be delivered immediately without blocking on the playback thread. The default {@link 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 97658d2906..dd8fbee53c 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 @@ -62,7 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; - @Nullable private MediaSource mediaSource; + private MediaSource mediaSource; private boolean playWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason; @RepeatMode private int repeatMode; @@ -219,38 +219,34 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override - @Deprecated public void prepare(MediaSource mediaSource) { - setMediaItem(mediaSource); - prepareInternal(/* resetPosition= */ true, /* resetState= */ true); + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override - @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - setMediaItem(mediaSource); - prepareInternal(resetPosition, resetState); + this.mediaSource = mediaSource; + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, + resetState, + /* resetError= */ true, + /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; + internalPlayer.prepare(mediaSource, resetPosition, resetState); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } - @Override - public void prepare() { - Assertions.checkNotNull(mediaSource); - prepareInternal(/* resetPosition= */ false, /* resetState= */ true); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - if (!getCurrentTimeline().isEmpty()) { - stop(/* reset= */ true); - } - seekTo(/* windowIndex= */ 0, startPositionMs); - setMediaItem(mediaItem); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - mediaSource = mediaItem; - } @Override public void setPlayWhenReady(boolean playWhenReady) { @@ -610,29 +606,6 @@ import java.util.concurrent.CopyOnWriteArrayList; } } - /* package */ void prepareInternal(boolean resetPosition, boolean resetState) { - Assertions.checkNotNull(mediaSource); - PlaybackInfo playbackInfo = - getResetPlaybackInfo( - resetPosition, - resetState, - /* resetError= */ true, - /* playbackState= */ Player.STATE_BUFFERING); - // Trigger internal prepare first before updating the playback info and notifying external - // listeners to ensure that new operations issued in the listener notifications reach the - // player after this prepare. The internal player can't change the playback info immediately - // because it uses a callback. - hasPendingPrepare = true; - pendingOperationAcks++; - internalPlayer.prepare(mediaSource, resetPosition, resetState); - updatePlaybackInfo( - playbackInfo, - /* positionDiscontinuity= */ false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); - } - private void handlePlaybackParameters( PlaybackParameters playbackParameters, boolean operationAck) { if (operationAck) { 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 f1d01a114f..729dd150ae 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 @@ -1162,7 +1162,6 @@ public class SimpleExoPlayer extends BasePlayer } @Override - @SuppressWarnings("deprecation") public void retry() { verifyApplicationThread(); if (mediaSource != null @@ -1172,38 +1171,23 @@ public class SimpleExoPlayer extends BasePlayer } @Override - @Deprecated - @SuppressWarnings("deprecation") public void prepare(MediaSource mediaSource) { prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override - @Deprecated public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { verifyApplicationThread(); - setMediaItem(mediaSource); - prepareInternal(resetPosition, resetState); - } - - @Override - public void prepare() { - verifyApplicationThread(); - prepareInternal(/* resetPosition= */ false, /* resetState= */ true); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - verifyApplicationThread(); - setMediaItemInternal(mediaItem); - player.setMediaItem(mediaItem, startPositionMs); - } - - @Override - public void setMediaItem(MediaSource mediaItem) { - verifyApplicationThread(); - setMediaItemInternal(mediaItem); - player.setMediaItem(mediaItem); + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + this.mediaSource = mediaSource; + mediaSource.addEventListener(eventHandler, analyticsCollector); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + player.prepare(mediaSource, resetPosition, resetState); } @Override @@ -1448,23 +1432,6 @@ public class SimpleExoPlayer extends BasePlayer // Internal methods. - private void prepareInternal(boolean resetPosition, boolean resetState) { - Assertions.checkNotNull(mediaSource); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepareInternal(resetPosition, resetState); - } - - private void setMediaItemInternal(MediaSource mediaItem) { - if (mediaSource != null) { - mediaSource.removeEventListener(analyticsCollector); - analyticsCollector.resetForNewMediaSource(); - } - mediaSource = mediaItem; - mediaSource.addEventListener(eventHandler, analyticsCollector); - } - private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { 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 8bd6b1ba09..8ec8cca06c 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; @@ -36,10 +35,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.ClippingMediaSource; -import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.LoopingMediaSource; -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.MediaSourceEventListener.EventDispatcher; @@ -1588,7 +1584,6 @@ public final class ExoPlayerTest { AtomicInteger counter = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessagesFromStartPositionOnlyOnce") - .waitForTimelineChanged() .pause() .sendMessage( (messageType, payload) -> { @@ -2931,218 +2926,6 @@ public final class ExoPlayerTest { assertThat(seenPlaybackSuppression.get()).isFalse(); } - @Test - public void testDelegatingMediaSourceApproach() throws Exception { - Timeline fakeTimeline = - new FakeTimeline( - new TimelineWindowDefinition( - /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000)); - final ConcatenatingMediaSource underlyingSource = new ConcatenatingMediaSource(); - CompositeMediaSource delegatingMediaSource = - new CompositeMediaSource() { - @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - super.prepareSourceInternal(mediaTransferListener); - underlyingSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); - underlyingSource.addMediaSource( - new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); - prepareChildSource(null, underlyingSource); - } - - @Override - public MediaPeriod createPeriod( - MediaPeriodId id, Allocator allocator, long startPositionUs) { - return underlyingSource.createPeriod(id, allocator, startPositionUs); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - underlyingSource.releasePeriod(mediaPeriod); - } - - @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline) { - refreshSourceInfo(timeline); - } - }; - int[] currentWindowIndices = new int[1]; - long[] currentPlaybackPositions = new long[1]; - long[] windowCounts = new long[1]; - int seekToWindowIndex = 1; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testDelegatingMediaSourceApproach") - .seek(/* windowIndex= */ 1, /* positionMs= */ 5000) - .waitForSeekProcessed() - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - currentWindowIndices[0] = player.getCurrentWindowIndex(); - currentPlaybackPositions[0] = player.getCurrentPosition(); - windowCounts[0] = player.getCurrentTimeline().getWindowCount(); - } - }) - .build(); - ExoPlayerTestRunner exoPlayerTestRunner = - new Builder() - .setMediaSource(delegatingMediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - exoPlayerTestRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - assertArrayEquals(new long[] {2}, windowCounts); - assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); - assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); - } - - @Test - public void testSeekTo_windowIndexIsReset_deprecated() throws Exception { - FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); - LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); - final int[] windowIndex = {C.INDEX_UNSET}; - final long[] positionMs = {C.TIME_UNSET}; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekTo_windowIndexIsReset_deprecated") - .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .waitForSeekProcessed() - .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - //noinspection deprecation - player.prepare(mediaSource); - player.seekTo(/* positionMs= */ 5000); - } - }) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - windowIndex[0] = player.getCurrentWindowIndex(); - positionMs[0] = player.getCurrentPosition(); - } - }) - .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(loopingMediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS); - - assertThat(windowIndex[0]).isEqualTo(0); - assertThat(positionMs[0]).isAtLeast(5000L); - } - - @Test - public void testSeekTo_windowIndexIsReset() throws Exception { - FakeTimeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline); - LoopingMediaSource loopingMediaSource = new LoopingMediaSource(mediaSource, 2); - final int[] windowIndex = {C.INDEX_UNSET}; - final long[] positionMs = {C.TIME_UNSET}; - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSeekTo_windowIndexIsReset") - .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) - .waitForSeekProcessed() - .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 5000) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.setMediaItem(mediaSource, /* startPositionMs= */ 5000); - player.prepare(); - } - }) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - windowIndex[0] = player.getCurrentWindowIndex(); - positionMs[0] = player.getCurrentPosition(); - } - }) - .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(loopingMediaSource) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS); - - assertThat(windowIndex[0]).isEqualTo(0); - assertThat(positionMs[0]).isAtLeast(5000L); - } - - @Test - public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception { - CountDownLatch becomingNoisyHandlingDisabled = new CountDownLatch(1); - CountDownLatch becomingNoisyDelivered = new CountDownLatch(1); - PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled") - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.setHandleAudioBecomingNoisy(false); - becomingNoisyHandlingDisabled.countDown(); - - // Wait for the broadcast to be delivered from the main thread. - try { - becomingNoisyDelivered.await(); - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } - } - }) - .delay(1) // Handle pending messages on the playback thread. - .executeRunnable(playerStateGrabber) - .build(); - - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder().setActionSchedule(actionSchedule).build(context).start(); - becomingNoisyHandlingDisabled.await(); - deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - becomingNoisyDelivered.countDown(); - - testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - assertThat(playerStateGrabber.playWhenReady).isTrue(); - } - - @Test - public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception { - CountDownLatch becomingNoisyHandlingEnabled = new CountDownLatch(1); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled") - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - player.setHandleAudioBecomingNoisy(true); - becomingNoisyHandlingEnabled.countDown(); - } - }) - .waitForPlayWhenReady(false) // Becoming noisy should set playWhenReady = false - .play() - .build(); - - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder().setActionSchedule(actionSchedule).build(context).start(); - becomingNoisyHandlingEnabled.await(); - deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - - // If the player fails to handle becoming noisy, blockUntilActionScheduleFinished will time out - // and throw, causing the test to fail. - testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); - } - // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { 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 bf3cc90a78..d64a44ac04 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 @@ -431,8 +431,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.setMediaItem(mediaSource); - player.prepare(); + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 47f34712b9..18eaec2cd7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -96,11 +96,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void prepare() { - throw new UnsupportedOperationException(); - } - @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -111,16 +106,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public void setMediaItem(MediaSource mediaItem) { - throw new UnsupportedOperationException(); - } - - @Override - public void setMediaItem(MediaSource mediaItem, long startPositionMs) { - throw new UnsupportedOperationException(); - } - @Override public void setPlayWhenReady(boolean playWhenReady) { throw new UnsupportedOperationException(); From 21957bf7836cad8528a139b05c125bf0254036a3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 18 Nov 2019 05:41:30 +0000 Subject: [PATCH 737/807] Revert "add default methods isSingleWindow and getInitialTimeline to MediaSource interface" This reverts commit 01a4cf98d551bdac161f1b845fe00aa1ccffda47. --- .../google/android/exoplayer2/Timeline.java | 71 --------- .../source/ConcatenatingMediaSource.java | 17 -- .../exoplayer2/source/LoopingMediaSource.java | 27 +--- .../exoplayer2/source/MaskingMediaSource.java | 55 ++----- .../exoplayer2/source/MediaSource.java | 27 ---- .../android/exoplayer2/TimelineTest.java | 146 ------------------ .../exoplayer2/testutil/FakeTimeline.java | 34 +--- 7 files changed, 25 insertions(+), 352 deletions(-) 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 a860249478..ce1a58822c 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 @@ -19,7 +19,6 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; /** * A flexible representation of the structure of media. A timeline is able to represent the @@ -279,48 +278,6 @@ public abstract class Timeline { return positionInFirstPeriodUs; } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !getClass().equals(obj.getClass())) { - return false; - } - Window that = (Window) obj; - return Util.areEqual(uid, that.uid) - && Util.areEqual(tag, that.tag) - && Util.areEqual(manifest, that.manifest) - && presentationStartTimeMs == that.presentationStartTimeMs - && windowStartTimeMs == that.windowStartTimeMs - && isSeekable == that.isSeekable - && isDynamic == that.isDynamic - && isLive == that.isLive - && defaultPositionUs == that.defaultPositionUs - && durationUs == that.durationUs - && firstPeriodIndex == that.firstPeriodIndex - && lastPeriodIndex == that.lastPeriodIndex - && positionInFirstPeriodUs == that.positionInFirstPeriodUs; - } - - @Override - public int hashCode() { - int result = 7; - result = 31 * result + uid.hashCode(); - result = 31 * result + (tag == null ? 0 : tag.hashCode()); - result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); - result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); - result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); - result = 31 * result + (isSeekable ? 1 : 0); - result = 31 * result + (isDynamic ? 1 : 0); - result = 31 * result + (isLive ? 1 : 0); - result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + firstPeriodIndex; - result = 31 * result + lastPeriodIndex; - result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); - return result; - } } /** @@ -577,34 +534,6 @@ public abstract class Timeline { return adPlaybackState.adResumePositionUs; } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !getClass().equals(obj.getClass())) { - return false; - } - Period that = (Period) obj; - return Util.areEqual(id, that.id) - && Util.areEqual(uid, that.uid) - && windowIndex == that.windowIndex - && durationUs == that.durationUs - && positionInWindowUs == that.positionInWindowUs - && Util.areEqual(adPlaybackState, that.adPlaybackState); - } - - @Override - public int hashCode() { - int result = 7; - result = 31 * result + (id == null ? 0 : id.hashCode()); - result = 31 * result + (uid == null ? 0 : uid.hashCode()); - result = 31 * result + windowIndex; - result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); - result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); - result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode()); - return result; - } } /** An empty timeline. */ 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 545b8f5155..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 @@ -139,23 +139,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource { - private final MaskingMediaSource maskingMediaSource; + private final MediaSource childSource; private final int loopCount; private final Map childMediaPeriodIdToMediaPeriodId; private final Map mediaPeriodToChildMediaPeriodId; @@ -58,7 +58,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { */ public LoopingMediaSource(MediaSource childSource, int loopCount) { Assertions.checkArgument(loopCount > 0); - this.maskingMediaSource = new MaskingMediaSource(childSource, /* useLazyPreparation= */ false); + this.childSource = childSource; this.loopCount = loopCount; childMediaPeriodIdToMediaPeriodId = new HashMap<>(); mediaPeriodToChildMediaPeriodId = new HashMap<>(); @@ -67,45 +67,32 @@ public final class LoopingMediaSource extends CompositeMediaSource { @Override @Nullable public Object getTag() { - return maskingMediaSource.getTag(); - } - - @Nullable - @Override - public Timeline getInitialTimeline() { - return loopCount != Integer.MAX_VALUE - ? new LoopingTimeline(maskingMediaSource.getTimeline(), loopCount) - : new InfinitelyLoopingTimeline(maskingMediaSource.getTimeline()); - } - - @Override - public boolean isSingleWindow() { - return false; + return childSource.getTag(); } @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); - prepareChildSource(/* id= */ null, maskingMediaSource); + prepareChildSource(/* id= */ null, childSource); } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { if (loopCount == Integer.MAX_VALUE) { - return maskingMediaSource.createPeriod(id, allocator, startPositionUs); + return childSource.createPeriod(id, allocator, startPositionUs); } Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); MediaPeriod mediaPeriod = - maskingMediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - maskingMediaSource.releasePeriod(mediaPeriod); + childSource.releasePeriod(mediaPeriod); MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod); if (childMediaPeriodId != null) { childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId); 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 index 031d50e7d2..891cb351c1 100644 --- 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 @@ -43,7 +43,6 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher; private boolean hasStartedPreparing; private boolean isPrepared; - private boolean hasRealTimeline; /** * Creates the masking media source. @@ -55,22 +54,14 @@ public final class MaskingMediaSource extends CompositeMediaSource { */ public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { this.mediaSource = mediaSource; - this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow(); + this.useLazyPreparation = useLazyPreparation; window = new Timeline.Window(); period = new Timeline.Period(); - Timeline initialTimeline = mediaSource.getInitialTimeline(); - if (initialTimeline != null) { - timeline = - MaskingTimeline.createWithRealTimeline( - initialTimeline, /* firstWindowUid= */ null, /* firstPeriodUid= */ null); - hasRealTimeline = true; - } else { - timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); - } + timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); } /** Returns the {@link Timeline}. */ - public synchronized Timeline getTimeline() { + public Timeline getTimeline() { return timeline; } @@ -138,16 +129,14 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - protected synchronized void onChildSourceInfoRefreshed( + protected void onChildSourceInfoRefreshed( Void id, MediaSource mediaSource, Timeline newTimeline) { if (isPrepared) { timeline = timeline.cloneWithUpdatedTimeline(newTimeline); } else if (newTimeline.isEmpty()) { timeline = - hasRealTimeline - ? timeline.cloneWithUpdatedTimeline(newTimeline) - : MaskingTimeline.createWithRealTimeline( - newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID); + MaskingTimeline.createWithRealTimeline( + newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID); } else { // Determine first period and the start position. // This will be: @@ -175,10 +164,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { window, period, /* windowIndex= */ 0, windowStartPositionUs); Object periodUid = periodPosition.first; long periodPositionUs = periodPosition.second; - timeline = - hasRealTimeline - ? timeline.cloneWithUpdatedTimeline(newTimeline) - : MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid); + timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid); if (unpreparedMaskingMediaPeriod != null) { MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; maskingPeriod.overridePreparePositionUs(periodPositionUs); @@ -187,7 +173,6 @@ public final class MaskingMediaSource extends CompositeMediaSource { maskingPeriod.createPeriod(idInSource); } } - hasRealTimeline = true; isPrepared = true; refreshSourceInfo(this.timeline); } @@ -208,15 +193,13 @@ public final class MaskingMediaSource extends CompositeMediaSource { } private Object getInternalPeriodUid(Object externalPeriodUid) { - return timeline.replacedInternalPeriodUid != null - && externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID) + return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID) ? timeline.replacedInternalPeriodUid : externalPeriodUid; } private Object getExternalPeriodUid(Object internalPeriodUid) { - return timeline.replacedInternalPeriodUid != null - && timeline.replacedInternalPeriodUid.equals(internalPeriodUid) + return timeline.replacedInternalPeriodUid.equals(internalPeriodUid) ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : internalPeriodUid; } @@ -229,8 +212,8 @@ public final class MaskingMediaSource extends CompositeMediaSource { public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object(); - @Nullable private final Object replacedInternalWindowUid; - @Nullable private final Object replacedInternalPeriodUid; + private final Object replacedInternalWindowUid; + private final Object replacedInternalPeriodUid; /** * Returns an instance with a dummy timeline using the provided window tag. @@ -253,14 +236,12 @@ public final class MaskingMediaSource extends CompositeMediaSource { * assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}. */ public static MaskingTimeline createWithRealTimeline( - Timeline timeline, @Nullable Object firstWindowUid, @Nullable Object firstPeriodUid) { + Timeline timeline, Object firstWindowUid, Object firstPeriodUid) { return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid); } private MaskingTimeline( - Timeline timeline, - @Nullable Object replacedInternalWindowUid, - @Nullable Object replacedInternalPeriodUid) { + Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) { super(timeline); this.replacedInternalWindowUid = replacedInternalWindowUid; this.replacedInternalPeriodUid = replacedInternalPeriodUid; @@ -292,7 +273,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { timeline.getPeriod(periodIndex, period, setIds); - if (Util.areEqual(period.uid, replacedInternalPeriodUid) && setIds) { + if (Util.areEqual(period.uid, replacedInternalPeriodUid)) { period.uid = DUMMY_EXTERNAL_PERIOD_UID; } return period; @@ -301,9 +282,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public int getIndexOfPeriod(Object uid) { return timeline.getIndexOfPeriod( - DUMMY_EXTERNAL_PERIOD_UID.equals(uid) && replacedInternalPeriodUid != null - ? replacedInternalPeriodUid - : uid); + DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid); } @Override @@ -354,8 +333,8 @@ public final class MaskingMediaSource extends CompositeMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { return period.set( - /* id= */ setIds ? 0 : null, - /* uid= */ setIds ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : null, + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID, /* windowIndex= */ 0, /* durationUs = */ C.TIME_UNSET, /* positionInWindowUs= */ 0); 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 f6dd4d79a4..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 @@ -228,33 +228,6 @@ public interface MediaSource { */ void removeEventListener(MediaSourceEventListener eventListener); - /** - * Returns the initial dummy timeline that is returned immediately when the real timeline is not - * yet known, or null to let the player create an initial timeline. - * - *

        The initial timeline must use the same uids for windows and periods that the real timeline - * will use. It also must provide windows which are marked as dynamic to indicate that the window - * is expected to change when the real timeline arrives. - * - *

        Any media source which has multiple windows should typically provide such an initial - * timeline to make sure the player reports the correct number of windows immediately. - */ - @Nullable - default Timeline getInitialTimeline() { - return null; - } - - /** - * Returns true if the media source is guaranteed to never have zero or more than one window. - * - *

        The default implementation returns {@code true}. - * - * @return true if the source has exactly one window. - */ - default boolean isSingleWindow() { - return true; - } - /** Returns the tag set on the media source, or null if none was set. */ @Nullable default Object getTag() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 5110ad411c..d6e65cb34d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2; -import static com.google.common.truth.Truth.assertThat; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; @@ -60,148 +58,4 @@ public class TimelineTest { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } - - @Test - public void testWindowEquals() { - Timeline.Window window = new Timeline.Window(); - assertThat(window).isEqualTo(new Timeline.Window()); - - Timeline.Window otherWindow = new Timeline.Window(); - otherWindow.tag = new Object(); - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.manifest = new Object(); - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.presentationStartTimeMs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.windowStartTimeMs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isSeekable = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isDynamic = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.isLive = true; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.defaultPositionUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.durationUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.firstPeriodIndex = 1; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.lastPeriodIndex = 1; - assertThat(window).isNotEqualTo(otherWindow); - - otherWindow = new Timeline.Window(); - otherWindow.positionInFirstPeriodUs = C.TIME_UNSET; - assertThat(window).isNotEqualTo(otherWindow); - - window.uid = new Object(); - window.tag = new Object(); - window.manifest = new Object(); - window.presentationStartTimeMs = C.TIME_UNSET; - window.windowStartTimeMs = C.TIME_UNSET; - window.isSeekable = true; - window.isDynamic = true; - window.isLive = true; - window.defaultPositionUs = C.TIME_UNSET; - window.durationUs = C.TIME_UNSET; - window.firstPeriodIndex = 1; - window.lastPeriodIndex = 1; - window.positionInFirstPeriodUs = C.TIME_UNSET; - otherWindow = - otherWindow.set( - window.uid, - window.tag, - window.manifest, - window.presentationStartTimeMs, - window.windowStartTimeMs, - window.isSeekable, - window.isDynamic, - window.isLive, - window.defaultPositionUs, - window.durationUs, - window.firstPeriodIndex, - window.lastPeriodIndex, - window.positionInFirstPeriodUs); - assertThat(window).isEqualTo(otherWindow); - } - - @Test - public void testWindowHashCode() { - Timeline.Window window = new Timeline.Window(); - Timeline.Window otherWindow = new Timeline.Window(); - assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - - window.tag = new Object(); - assertThat(window.hashCode()).isNotEqualTo(otherWindow.hashCode()); - otherWindow.tag = window.tag; - assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode()); - } - - @Test - public void testPeriodEquals() { - Timeline.Period period = new Timeline.Period(); - assertThat(period).isEqualTo(new Timeline.Period()); - - Timeline.Period otherPeriod = new Timeline.Period(); - otherPeriod.id = new Object(); - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.uid = new Object(); - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.windowIndex = 12; - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - otherPeriod.durationUs = 11L; - assertThat(period).isNotEqualTo(otherPeriod); - - otherPeriod = new Timeline.Period(); - period.id = new Object(); - period.uid = new Object(); - period.windowIndex = 1; - period.durationUs = 123L; - otherPeriod = - otherPeriod.set( - period.id, - period.uid, - period.windowIndex, - period.durationUs, - /* positionInWindowUs= */ 0); - assertThat(period).isEqualTo(otherPeriod); - } - - @Test - public void testPeriodHashCode() { - Timeline.Period period = new Timeline.Period(); - Timeline.Period otherPeriod = new Timeline.Period(); - assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); - - period.windowIndex = 12; - assertThat(period.hashCode()).isNotEqualTo(otherPeriod.hashCode()); - otherPeriod.windowIndex = period.windowIndex; - assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode()); - } } 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 c8c7190007..401fcf8034 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 @@ -40,7 +40,6 @@ public final class FakeTimeline extends Timeline { public final Object id; public final boolean isSeekable; public final boolean isDynamic; - public final boolean isLive; public final long durationUs; public final AdPlaybackState adPlaybackState; @@ -100,41 +99,10 @@ public final class FakeTimeline extends Timeline { boolean isDynamic, long durationUs, AdPlaybackState adPlaybackState) { - this( - periodCount, - id, - isSeekable, - isDynamic, - /* isLive= */ isDynamic, - durationUs, - adPlaybackState); - } - - /** - * Creates a window definition with ad groups. - * - * @param periodCount The number of periods in the window. Each period get an equal slice of the - * total window duration. - * @param id The UID of the window. - * @param isSeekable Whether the window is seekable. - * @param isDynamic Whether the window is dynamic. - * @param isLive Whether the window is live. - * @param durationUs The duration of the window in microseconds. - * @param adPlaybackState The ad playback state. - */ - public TimelineWindowDefinition( - int periodCount, - Object id, - boolean isSeekable, - boolean isDynamic, - boolean isLive, - long durationUs, - AdPlaybackState adPlaybackState) { this.periodCount = periodCount; this.id = id; this.isSeekable = isSeekable; this.isDynamic = isDynamic; - this.isLive = isLive; this.durationUs = durationUs; this.adPlaybackState = adPlaybackState; } @@ -221,7 +189,7 @@ public final class FakeTimeline extends Timeline { /* windowStartTimeMs= */ C.TIME_UNSET, windowDefinition.isSeekable, windowDefinition.isDynamic, - windowDefinition.isLive, + /* isLive= */ windowDefinition.isDynamic, /* defaultPositionUs= */ 0, windowDefinition.durationUs, periodOffsets[windowIndex], From 07bfab8e4c2b9be619a710b402c9a103cd1bb5ce Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 18 Nov 2019 11:09:34 +0000 Subject: [PATCH 738/807] document media button handling prior to API level 21 ISSUE: #6545 PiperOrigin-RevId: 281032120 --- .../ext/mediasession/MediaSessionConnector.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 cce2ebfc28..84d5fea0c7 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 @@ -30,6 +30,7 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Pair; +import android.view.KeyEvent; import androidx.annotation.LongDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -500,6 +501,17 @@ public final class MediaSessionConnector { * Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}. * + *

        Please note that prior to API 21 MediaButton events are not delivered to the {@link + * MediaSessionCompat}. Instead they are delivered as key events (see 'Responding to media + * buttons'). In an {@link android.app.Activity Activity}, media button events arrive at the + * {@link android.app.Activity#dispatchKeyEvent(KeyEvent)} method. + * + *

        If you are running the player in a foreground service (prior to API 21), you can create an + * intent filter and handle the {@code android.intent.action.MEDIA_BUTTON} action yourself. See + * Service handling ACTION_MEDIA_BUTTON for more information. + * * @param mediaButtonEventHandler The {@link MediaButtonEventHandler}, or null to let the event be * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}. */ From 35d9bdea097992033fd75f210267a3cc729fe4b0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Nov 2019 11:50:50 +0000 Subject: [PATCH 739/807] Add Util.linearSearch PiperOrigin-RevId: 281037183 --- .../google/android/exoplayer2/util/Util.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) 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 a6e49fd645..23447acddf 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 @@ -713,11 +713,29 @@ public final class Util { return result; } + /** + * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link + * C#INDEX_UNSET} if {@code value} is not contained in {@code array}. + * + * @param array The array to search. + * @param value The value to search for. + * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET} + * if {@code value} is not contained in {@code array}. + */ + public static int linearSearch(int[] array, int value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) { + return i; + } + } + return C.INDEX_UNSET; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. - *

        - * The search is performed using a binary search algorithm, so the array must be sorted. If the + * + *

        The search is performed using a binary search algorithm, so the array must be sorted. If the * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the * index of the first one will be returned. * @@ -731,8 +749,8 @@ public final class Util { * @return The index of the largest element in {@code array} that is less than (or optionally * equal to) {@code value}. */ - public static int binarySearchFloor(int[] array, int value, boolean inclusive, - boolean stayInBounds) { + public static int binarySearchFloor( + int[] array, int value, boolean inclusive, boolean stayInBounds) { int index = Arrays.binarySearch(array, value); if (index < 0) { index = -(index + 2); From 8c848a2a53cf955d813df827ee6512501c127886 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 18 Nov 2019 12:07:59 +0000 Subject: [PATCH 740/807] Remove option to disable loop filter for VP9 PiperOrigin-RevId: 281039634 --- .../exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 13 ++----------- .../android/exoplayer2/ext/vp9/VpxDecoder.java | 4 +--- 2 files changed, 3 insertions(+), 14 deletions(-) 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 31c663c6eb..4e7ad65642 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 @@ -68,7 +68,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; private final boolean enableRowMultiThreadMode; - private final boolean disableLoopFilter; private final int threads; private VpxDecoder decoder; @@ -102,8 +101,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { eventListener, maxDroppedFramesToNotify, /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - /* disableLoopFilter= */ false); + /* playClearSamplesWithoutKeys= */ false); } /** @@ -121,7 +119,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * 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 disableLoopFilter Disable the libvpx in-loop smoothing filter. */ public LibvpxVideoRenderer( long allowedJoiningTimeMs, @@ -129,8 +126,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - boolean disableLoopFilter) { + boolean playClearSamplesWithoutKeys) { this( allowedJoiningTimeMs, eventHandler, @@ -138,7 +134,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys, - disableLoopFilter, /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), /* numInputBuffers= */ 4, @@ -160,7 +155,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * 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 disableLoopFilter Disable the libvpx in-loop smoothing filter. * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @param numInputBuffers Number of input buffers. @@ -173,7 +167,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads, int numInputBuffers, @@ -185,7 +178,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys); - this.disableLoopFilter = disableLoopFilter; this.enableRowMultiThreadMode = enableRowMultiThreadMode; this.threads = threads; this.numInputBuffers = numInputBuffers; @@ -225,7 +217,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { numOutputBuffers, initialInputBufferSize, mediaCrypto, - disableLoopFilter, enableRowMultiThreadMode, threads); TraceUtil.endSection(); 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 1af3644744..b4535a3e9c 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 @@ -53,7 +53,6 @@ import java.nio.ByteBuffer; * @param initialInputBufferSize The initial size of each input buffer. * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. - * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. @@ -63,7 +62,6 @@ import java.nio.ByteBuffer; int numOutputBuffers, int initialInputBufferSize, @Nullable ExoMediaCrypto exoMediaCrypto, - boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { @@ -77,7 +75,7 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(disableLoopFilter, enableRowMultiThreadMode, threads); + vpxDecContext = vpxInit(/* disableLoopFilter= */ false, enableRowMultiThreadMode, threads); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } From 09df3a013c70fd232bebe7fc4b1a547c4b4022f4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Nov 2019 13:27:40 +0000 Subject: [PATCH 741/807] Don't check rotated resolution for HEVC on LG Q7 Issue: #6612 PiperOrigin-RevId: 281048324 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 601ddf9b9c..64517feec9 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 @@ -405,10 +405,8 @@ public final class MediaCodecInfo { return false; } if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) { - // Capabilities are known to be inaccurately reported for vertical resolutions on some devices - // (b/31387661). If the video is vertical and the capabilities indicate support if the width - // and height are swapped, we assume that the vertical resolution is also supported. if (width >= height + || !enableRotatedVerticalResolutionWorkaround(name) || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; @@ -599,4 +597,21 @@ public final class MediaCodecInfo { private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { return capabilities.getMaxSupportedInstances(); } + + /** + * Capabilities are known to be inaccurately reported for vertical resolutions on some devices. + * [Internal ref: b/31387661]. When this workaround is enabled, we also check whether the + * capabilities indicate support if the width and height are swapped. If they do, we assume that + * the vertical resolution is also supported. + * + * @param name The name of the codec. + * @return Whether to enable the workaround. + */ + private static final boolean enableRotatedVerticalResolutionWorkaround(String name) { + if ("OMX.MTK.VIDEO.DECODER.HEVC".equals(name) && "mcv5a".equals(Util.DEVICE)) { + // See https://github.com/google/ExoPlayer/issues/6612. + return false; + } + return true; + } } From 6b03ce8f1234914a35efae95ba5d70df93d06b8c Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Nov 2019 13:38:06 +0000 Subject: [PATCH 742/807] Remove SimpleCache hacks that are no longer used PiperOrigin-RevId: 281049383 --- .../upstream/cache/SimpleCache.java | 39 +------------------ 1 file changed, 2 insertions(+), 37 deletions(-) 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 e618fcad75..cf8056f5a9 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 @@ -61,9 +61,6 @@ public final class SimpleCache implements Cache { private static final HashSet lockedCacheDirs = new HashSet<>(); - private static boolean cacheFolderLockingDisabled; - private static boolean cacheInitializationExceptionsDisabled; - private final File cacheDir; private final CacheEvictor evictor; private final CachedContentIndex contentIndex; @@ -85,33 +82,6 @@ public final class SimpleCache implements Cache { return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile()); } - /** - * Disables locking the cache folders which {@link SimpleCache} instances are using and releases - * any previous lock. - * - *

        The locking prevents multiple {@link SimpleCache} instances from being created for the same - * folder. Disabling it may cause the cache data to be corrupted. Use at your own risk. - * - * @deprecated Don't create multiple {@link SimpleCache} instances for the same cache folder. If - * you need to create another instance, make sure you call {@link #release()} on the previous - * instance. - */ - @Deprecated - public static synchronized void disableCacheFolderLocking() { - cacheFolderLockingDisabled = true; - lockedCacheDirs.clear(); - } - - /** - * Disables throwing of cache initialization exceptions. - * - * @deprecated Don't use this. Provided for problematic upgrade cases only. - */ - @Deprecated - public static void disableCacheInitializationExceptions() { - cacheInitializationExceptionsDisabled = true; - } - /** * Deletes all content belonging to a cache instance. * @@ -304,7 +274,7 @@ public final class SimpleCache implements Cache { * @throws CacheException If an error occurred during initialization. */ public synchronized void checkInitialization() throws CacheException { - if (!cacheInitializationExceptionsDisabled && initializationException != null) { + if (initializationException != null) { throw initializationException; } } @@ -828,15 +798,10 @@ public final class SimpleCache implements Cache { } private static synchronized boolean lockFolder(File cacheDir) { - if (cacheFolderLockingDisabled) { - return true; - } return lockedCacheDirs.add(cacheDir.getAbsoluteFile()); } private static synchronized void unlockFolder(File cacheDir) { - if (!cacheFolderLockingDisabled) { - lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); - } + lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); } } From 7f19b8850659b0fe0bf27f255a1e71c7e8230f1d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Nov 2019 13:42:57 +0000 Subject: [PATCH 743/807] Make some listener methods default PiperOrigin-RevId: 281050034 --- .../drm/DefaultDrmSessionEventListener.java | 8 +- .../DefaultMediaSourceEventListener.java | 77 +------------------ .../source/ExtractorMediaSource.java | 2 +- .../source/MediaSourceEventListener.java | 30 ++++---- .../source/SingleSampleMediaSource.java | 2 +- .../source/ClippingMediaSourceTest.java | 2 +- 6 files changed, 26 insertions(+), 95 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java index fa5b60e66e..297f26bb71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java @@ -24,7 +24,7 @@ public interface DefaultDrmSessionEventListener { default void onDrmSessionAcquired() {} /** Called each time keys are loaded. */ - void onDrmKeysLoaded(); + default void onDrmKeysLoaded() {} /** * Called when a drm error occurs. @@ -38,13 +38,13 @@ public interface DefaultDrmSessionEventListener { * * @param error The corresponding exception. */ - void onDrmSessionManagerError(Exception error); + default void onDrmSessionManagerError(Exception error) {} /** Called each time offline keys are restored. */ - void onDrmKeysRestored(); + default void onDrmKeysRestored() {} /** Called each time offline keys are removed. */ - void onDrmKeysRemoved(); + default void onDrmKeysRemoved() {} /** Called each time a drm session is released. */ default void onDrmSessionReleased() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java index 14bafdaf4b..fbb3a86221 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java @@ -15,78 +15,9 @@ */ package com.google.android.exoplayer2.source; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import java.io.IOException; - /** - * A {@link MediaSourceEventListener} allowing selective overrides. All methods are implemented as - * no-ops. + * @deprecated Use {@link MediaSourceEventListener} interface directly for selective overrides as + * all methods are implemented as no-op default methods. */ -public abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener { - - @Override - public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - // Do nothing. - } - - @Override - public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { - // Do nothing. - } - - @Override - public void onLoadStarted( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData) { - // Do nothing. - } - - @Override - public void onLoadCompleted( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData) { - // Do nothing. - } - - @Override - public void onLoadCanceled( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData) { - // Do nothing. - } - - @Override - public void onLoadError( - int windowIndex, - @Nullable MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData, - IOException error, - boolean wasCanceled) { - // Do nothing. - } - - @Override - public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { - // Do nothing. - } - - @Override - public void onUpstreamDiscarded( - int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - // Do nothing. - } - - @Override - public void onDownstreamFormatChanged( - int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - // Do nothing. - } -} +@Deprecated +public abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener {} 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 ee731cbc09..060027fee7 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 @@ -365,7 +365,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource { } @Deprecated - private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { + private static final class EventListenerWrapper implements MediaSourceEventListener { private final EventListener eventListener; 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 ab8d86cc55..9e6f4f9cf1 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 @@ -164,7 +164,7 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the created media period. */ - void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId); + default void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when a media period is released by the media source. @@ -172,7 +172,7 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the released media period. */ - void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId); + default void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when a load begins. @@ -185,11 +185,11 @@ public interface MediaSourceEventListener { * LoadEventInfo#responseHeaders} will be empty. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadStarted( + default void onLoadStarted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData); + MediaLoadData mediaLoadData) {} /** * Called when a load ends. @@ -203,11 +203,11 @@ public interface MediaSourceEventListener { * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadCompleted( + default void onLoadCompleted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData); + MediaLoadData mediaLoadData) {} /** * Called when a load is canceled. @@ -221,11 +221,11 @@ public interface MediaSourceEventListener { * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadCanceled( + default void onLoadCanceled( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData); + MediaLoadData mediaLoadData) {} /** * Called when a load error occurs. @@ -252,13 +252,13 @@ public interface MediaSourceEventListener { * @param error The load error. * @param wasCanceled Whether the load was canceled as a result of the error. */ - void onLoadError( + default void onLoadError( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, - boolean wasCanceled); + boolean wasCanceled) {} /** * Called when a media period is first being read from. @@ -266,7 +266,7 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from. */ - void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId); + default void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when data is removed from the back of a media buffer, typically so that it can be @@ -276,8 +276,8 @@ public interface MediaSourceEventListener { * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. */ - void onUpstreamDiscarded( - int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData); + default void onUpstreamDiscarded( + int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} /** * Called when a downstream format change occurs (i.e. when the format of the media being read @@ -287,8 +287,8 @@ public interface MediaSourceEventListener { * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. * @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data. */ - void onDownstreamFormatChanged( - int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData); + default void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} /** Dispatches events to {@link MediaSourceEventListener}s. */ final class EventDispatcher { 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 be939fd018..db1414942f 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 @@ -347,7 +347,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { */ @Deprecated @SuppressWarnings("deprecation") - private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { + private static final class EventListenerWrapper implements MediaSourceEventListener { private final EventListener eventListener; private final int eventSourceId; 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 532ad61b85..b2dc666790 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 @@ -555,7 +555,7 @@ public final class ClippingMediaSourceTest { () -> clippingMediaSource.addEventListener( new Handler(), - new DefaultMediaSourceEventListener() { + new MediaSourceEventListener() { @Override public void onDownstreamFormatChanged( int windowIndex, From da9c985cce3e61190e4a2276b9e171a33831b66b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 18 Nov 2019 13:50:01 +0000 Subject: [PATCH 744/807] Fix byte order for HDR10+ static metadata The implementation of writing HDR10+ static metadata assumed that the application would use default (big endian) byte order for this metadata but MediaCodec expects the order to match the specification CTA-861.3. PiperOrigin-RevId: 281050806 --- RELEASENOTES.md | 1 + .../exoplayer2/extractor/mkv/MatroskaExtractor.java | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5e4c9b78aa..6451fada36 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,6 +10,7 @@ [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid `ExoMediaDrm` leaks ([#4721](https://github.com/google/ExoPlayer/issues/4721)). +* Fix byte order of HDR10+ static metadata to match CTA-861.3. ### 2.11.0 (not yet released) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 517c087e18..69bdb2cd46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1912,9 +1912,9 @@ public class MatroskaExtractor implements Extractor { initializationData = new ArrayList<>(3); initializationData.add(codecPrivate); initializationData.add( - ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(codecDelayNs).array()); + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array()); initializationData.add( - ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(seekPreRollNs).array()); + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array()); break; case CODEC_ID_AAC: mimeType = MimeTypes.AUDIO_AAC; @@ -2116,6 +2116,7 @@ public class MatroskaExtractor implements Extractor { } /** Returns the HDR Static Info as defined in CTA-861.3. */ + @Nullable private byte[] getHdrStaticInfo() { // Are all fields present. if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE @@ -2128,7 +2129,7 @@ public class MatroskaExtractor implements Extractor { } byte[] hdrStaticInfoData = new byte[25]; - ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData); + ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData).order(ByteOrder.LITTLE_ENDIAN); hdrStaticInfo.put((byte) 0); // Type. hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f)); hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f)); From 8c3e6663d3213a1d5e5549666b35b63ec0e4f6cd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Nov 2019 14:00:00 +0000 Subject: [PATCH 745/807] Clean up non-trivial track selection deprecation PiperOrigin-RevId: 281051893 --- .../exoplayer2/offline/DownloadHelper.java | 12 +++++ .../trackselection/TrackSelection.java | 44 ++++--------------- .../testutil/MediaPeriodAsserts.java | 12 +++++ 3 files changed, 33 insertions(+), 35 deletions(-) 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 2e64d9f421..c585c79b76 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 @@ -37,6 +37,8 @@ import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; @@ -1100,6 +1102,16 @@ public final class DownloadHelper { public Object getSelectionData() { return null; } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + // Do nothing. + } } private static final class DummyBandwidthMeter implements BandwidthMeter { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index bd99403b07..ad1a6ef1f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelectionUtil.AdaptiveTrackSelectionFactory; import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -77,32 +76,19 @@ public interface TrackSelection { interface Factory { /** - * @deprecated Implement {@link #createTrackSelections(Definition[], BandwidthMeter)} instead. - * Calling {@link TrackSelectionUtil#createTrackSelectionsForDefinitions(Definition[], - * AdaptiveTrackSelectionFactory)} helps to create a single adaptive track selection in the - * same way as using this deprecated method. - */ - @Deprecated - default TrackSelection createTrackSelection( - TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { - throw new UnsupportedOperationException(); - } - - /** - * Creates a new selection for each {@link Definition}. + * Creates track selections for the provided {@link Definition Definitions}. + * + *

        Implementations that create at most one adaptive track selection may use {@link + * TrackSelectionUtil#createTrackSelectionsForDefinitions}. * * @param definitions A {@link Definition} array. May include null values. * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @return The created selections. Must have the same length as {@code definitions} and may * include null values. */ - @SuppressWarnings("deprecation") - default @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> createTrackSelection(definition.group, bandwidthMeter, definition.tracks)); - } + @NullableType + TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter); } /** @@ -213,16 +199,6 @@ public interface TrackSelection { */ default void onDiscontinuity() {} - /** - * @deprecated Use and implement {@link #updateSelectedTrack(long, long, long, List, - * MediaChunkIterator[])} instead. - */ - @Deprecated - default void updateSelectedTrack( - long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { - throw new UnsupportedOperationException(); - } - /** * Updates the selected track for sources that load media in discrete {@link MediaChunk}s. * @@ -247,14 +223,12 @@ public interface TrackSelection { * that this information may not be available for all tracks, and so some iterators may be * empty. */ - default void updateSelectedTrack( + void updateSelectedTrack( long playbackPositionUs, long bufferedDurationUs, long availableDurationUs, List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateSelectedTrack(playbackPositionUs, bufferedDurationUs, availableDurationUs); - } + MediaChunkIterator[] mediaChunkIterators); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 5ee3e9561e..42fc40e72d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.ConditionVariable; @@ -234,5 +236,15 @@ public final class MediaPeriodAsserts { public Object getSelectionData() { return null; } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + // Do nothing. + } } } From 6158b2fa2a38b9a18bf0fc2138682adb0161d993 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Nov 2019 15:32:46 +0000 Subject: [PATCH 746/807] Allow user to pick which track types to create placeholder sessions for Issue:#4867 PiperOrigin-RevId: 281064793 --- .../drm/DefaultDrmSessionManager.java | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) 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 a65f45309a..e1c7793f8c 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 @@ -57,7 +57,7 @@ public class DefaultDrmSessionManager implements DrmSe private UUID uuid; private ExoMediaDrm.Provider exoMediaDrmProvider; private boolean multiSession; - private boolean preferSecureDecoders; + private int[] useDrmSessionsForClearContentTrackTypes; @Flags private int flags; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -70,7 +70,7 @@ public class DefaultDrmSessionManager implements DrmSe *

      • {@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link * FrameworkMediaDrm#DEFAULT_PROVIDER}. *
      • {@link #setMultiSession multiSession}: {@code false}. - *
      • {@link #setPreferSecureDecoders preferSecureDecoders}: {@code false}. + *
      • {@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks. *
      • {@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}. *
      • {@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link * DefaultLoadErrorHandlingPolicy}. @@ -82,6 +82,7 @@ public class DefaultDrmSessionManager implements DrmSe uuid = C.WIDEVINE_UUID; exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + useDrmSessionsForClearContentTrackTypes = new int[0]; } /** @@ -127,14 +128,27 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Sets whether this session manager should hint the use of secure decoders for clear content. + * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear + * sections of the media content. * - * @param preferSecureDecoders Whether this session manager should hint the use of secure - * decoders for clear content. + *

        Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders + * when transitioning between clear and encrypted sections of content. + * + * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO} + * and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of + * whether the content is clear or encrypted. * @return This builder. + * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains + * track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}. */ - public Builder setPreferSecureDecoders(boolean preferSecureDecoders) { - this.preferSecureDecoders = preferSecureDecoders; + public Builder setUseDrmSessionsForClearContent( + int... useDrmSessionsForClearContentTrackTypes) { + for (int trackType : useDrmSessionsForClearContentTrackTypes) { + Assertions.checkArgument( + trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO); + } + this.useDrmSessionsForClearContentTrackTypes = + useDrmSessionsForClearContentTrackTypes.clone(); return this; } @@ -174,7 +188,7 @@ public class DefaultDrmSessionManager implements DrmSe mediaDrmCallback, keyRequestParameters, multiSession, - preferSecureDecoders, + useDrmSessionsForClearContentTrackTypes, flags, loadErrorHandlingPolicy); } @@ -226,7 +240,7 @@ public class DefaultDrmSessionManager implements DrmSe @Nullable private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; private final boolean multiSession; - private final boolean preferSecureDecoders; + private final int[] useDrmSessionsForClearContentTrackTypes; @Flags private final int flags; private final ProvisioningManagerImpl provisioningManagerImpl; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -320,7 +334,7 @@ public class DefaultDrmSessionManager implements DrmSe callback, optionalKeyRequestParameters, multiSession, - /* preferSecureDecoders= */ false, + /* useDrmSessionsForClearContentTrackTypes= */ new int[0], /* flags= */ 0, new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } @@ -333,7 +347,7 @@ public class DefaultDrmSessionManager implements DrmSe MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters, boolean multiSession, - boolean preferSecureDecoders, + int[] useDrmSessionsForClearContentTrackTypes, @Flags int flags, LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkNotNull(uuid); @@ -344,7 +358,7 @@ public class DefaultDrmSessionManager implements DrmSe this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; - this.preferSecureDecoders = preferSecureDecoders; + this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; this.flags = flags; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; provisioningManagerImpl = new ProvisioningManagerImpl(); @@ -375,7 +389,7 @@ public class DefaultDrmSessionManager implements DrmSe /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be * called before {@link #acquireSession(Looper, DrmInitData)} or {@link - * #acquirePlaceholderSession(Looper)} is called. + * #acquirePlaceholderSession} is called. * *

        By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when * required. @@ -460,15 +474,13 @@ public class DefaultDrmSessionManager implements DrmSe @Nullable public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { assertExpectedPlaybackLooper(playbackLooper); - Assertions.checkNotNull(exoMediaDrm); + ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); boolean avoidPlaceholderDrmSessions = FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; // Avoid attaching a session to sparse formats. - avoidPlaceholderDrmSessions |= - trackType != C.TRACK_TYPE_VIDEO && trackType != C.TRACK_TYPE_AUDIO; if (avoidPlaceholderDrmSessions - || !preferSecureDecoders + || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET || exoMediaDrm.getExoMediaCryptoType() == null) { return null; } From d99b2c35091ab15cf0dead94b8f151bd8f0e73b3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Nov 2019 16:56:52 +0000 Subject: [PATCH 747/807] Cleanup key request parameters - Make NonNull, which is already the case when using the manager builder. - Better document PLAYREADY_CUSTOM_DATA_KEY, now that newPlayReadyInstance is no more. PiperOrigin-RevId: 281079288 --- .../exoplayer2/drm/DefaultDrmSession.java | 11 +++-- .../drm/DefaultDrmSessionManager.java | 40 ++++++++++--------- 2 files changed, 27 insertions(+), 24 deletions(-) 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 25a08c9058..0c36bc7d0f 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 @@ -107,7 +107,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; private final boolean isPlaceholderSession; - @Nullable private final HashMap optionalKeyRequestParameters; + private final HashMap keyRequestParameters; private final EventDispatcher eventDispatcher; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -140,7 +140,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param isPlaceholderSession Whether this session is not expected to acquire any keys. * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using * offline keys. - * @param optionalKeyRequestParameters The optional key request parameters. + * @param keyRequestParameters Key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. * @param eventDispatcher The dispatcher for DRM session manager events. @@ -156,7 +156,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @DefaultDrmSessionManager.Mode int mode, boolean isPlaceholderSession, @Nullable byte[] offlineLicenseKeySetId, - @Nullable HashMap optionalKeyRequestParameters, + HashMap keyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, EventDispatcher eventDispatcher, @@ -177,7 +177,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else { this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas)); } - this.optionalKeyRequestParameters = optionalKeyRequestParameters; + this.keyRequestParameters = keyRequestParameters; this.callback = callback; this.eventDispatcher = eventDispatcher; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; @@ -417,8 +417,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private void postKeyRequest(byte[] scope, int type, boolean allowRetry) { try { - currentKeyRequest = - mediaDrm.getKeyRequest(scope, schemeDatas, type, optionalKeyRequestParameters); + currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters); Util.castNonNull(requestHandler) .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); } catch (Exception 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 e1c7793f8c..f8e854e51d 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 @@ -86,7 +86,10 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Sets the parameters to pass to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. + * Sets the key request parameters to pass as the last argument to {@link + * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. + * + *

        Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}. * * @param keyRequestParameters A map with parameters. * @return This builder. @@ -206,7 +209,8 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. + * A key for specifying PlayReady custom data in the key request parameters passed to {@link + * Builder#setKeyRequestParameters(Map)}. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; @@ -237,7 +241,7 @@ public class DefaultDrmSessionManager implements DrmSe private final UUID uuid; private final ExoMediaDrm.Provider exoMediaDrmProvider; private final MediaDrmCallback callback; - @Nullable private final HashMap optionalKeyRequestParameters; + private final HashMap keyRequestParameters; private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int[] useDrmSessionsForClearContentTrackTypes; @@ -262,8 +266,8 @@ public class DefaultDrmSessionManager implements DrmSe * @param uuid The UUID of the drm scheme. * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. * @deprecated Use {@link Builder} instead. */ @SuppressWarnings("deprecation") @@ -272,12 +276,12 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters) { + @Nullable HashMap keyRequestParameters) { this( uuid, exoMediaDrm, callback, - optionalKeyRequestParameters, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, /* multiSession= */ false, INITIAL_DRM_REQUEST_RETRY_COUNT); } @@ -286,8 +290,8 @@ public class DefaultDrmSessionManager implements DrmSe * @param uuid The UUID of the drm scheme. * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. * @param multiSession A boolean that specify whether multiple key session support is enabled. * Default is false. * @deprecated Use {@link Builder} instead. @@ -297,13 +301,13 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters, + @Nullable HashMap keyRequestParameters, boolean multiSession) { this( uuid, exoMediaDrm, callback, - optionalKeyRequestParameters, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, multiSession, INITIAL_DRM_REQUEST_RETRY_COUNT); } @@ -312,8 +316,8 @@ public class DefaultDrmSessionManager implements DrmSe * @param uuid The UUID of the drm scheme. * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. * @param multiSession A boolean that specify whether multiple key session support is enabled. * Default is false. * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and @@ -325,14 +329,14 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters, + @Nullable HashMap keyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount) { this( uuid, new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm), callback, - optionalKeyRequestParameters, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, multiSession, /* useDrmSessionsForClearContentTrackTypes= */ new int[0], /* flags= */ 0, @@ -345,7 +349,7 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters, + HashMap keyRequestParameters, boolean multiSession, int[] useDrmSessionsForClearContentTrackTypes, @Flags int flags, @@ -355,7 +359,7 @@ public class DefaultDrmSessionManager implements DrmSe this.uuid = uuid; this.exoMediaDrmProvider = exoMediaDrmProvider; this.callback = callback; - this.optionalKeyRequestParameters = optionalKeyRequestParameters; + this.keyRequestParameters = keyRequestParameters; this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; @@ -576,7 +580,7 @@ public class DefaultDrmSessionManager implements DrmSe mode, isPlaceholderSession, offlineLicenseKeySetId, - optionalKeyRequestParameters, + keyRequestParameters, callback, Assertions.checkNotNull(playbackLooper), eventDispatcher, From 654b7aa12bb6009dceecb5597f9ae7eb9a999982 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Nov 2019 17:24:18 +0000 Subject: [PATCH 748/807] Release notes for 2.10.8 PiperOrigin-RevId: 281084720 --- RELEASENOTES.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6451fada36..a9fbe01e6e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -74,10 +74,6 @@ ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Fix Dolby Vision fallback to AVC and HEVC. * Audio: - * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect - channel count check. - * Handle new signaling for E-AC3 JOC audio in DASH - ([#6636](https://github.com/google/ExoPlayer/issues/6636)). * Fix the start of audio getting truncated when transitioning to a new item in a playlist of Opus streams. * Workaround broken raw audio decoding on Oppo R9 @@ -142,7 +138,24 @@ * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). -### 2.10.7 (2019-11-12) ### +### 2.10.8 (2019-11-19) ### + +* E-AC3 JOC + * Handle new signaling in DASH manifests + ([#6636](https://github.com/google/ExoPlayer/issues/6636)). + * Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect + channel count check. +* FLAC + * Fix sniffing for some FLAC streams. + * Fix FLAC `Format.bitrate` values. +* Parse ALAC channel count and sample rate information from a more robust source + when contained in MP4 + ([#6648](https://github.com/google/ExoPlayer/issues/6648)). +* Fix seeking into multi-period content in the edge case that the period + containing the seek position has just been removed + ([#6641](https://github.com/google/ExoPlayer/issues/6641)). + +### 2.10.7 (2019-11-06) ### * HLS: Fix detection of Dolby Atmos to match the HLS authoring specification. * MediaSession extension: Update shuffle and repeat modes when playback state From b5d993536164e961962e89bb6cc94e140505bec5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Nov 2019 17:32:48 +0000 Subject: [PATCH 749/807] Deprecate public renderer constructors that take a DrmSessionManager PiperOrigin-RevId: 281086336 --- .../ext/opus/LibopusAudioRenderer.java | 5 ++ .../ext/vp9/LibvpxVideoRenderer.java | 46 +++++++++++++++ .../audio/MediaCodecAudioRenderer.java | 57 +++++++++++++++++++ .../video/MediaCodecVideoRenderer.java | 46 +++++++++++++++ 4 files changed, 154 insertions(+) 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 0c0647595a..d17b6ebb53 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.MimeTypes; /** Decodes and renders audio using the native Opus decoder. */ @@ -66,7 +67,11 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. + * @deprecated Use {@link #LibopusAudioRenderer(Handler, AudioRendererEventListener, + * AudioProcessor...)} instead, and pass DRM-related parameters to the {@link MediaSource} + * factories. */ + @Deprecated public LibopusAudioRenderer( @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, 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 4e7ad65642..7fcb89dc12 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 @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; @@ -119,7 +120,12 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * 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. + * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, + * boolean, int, int, int)}} instead, and pass DRM-related parameters to the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public LibvpxVideoRenderer( long allowedJoiningTimeMs, @Nullable Handler eventHandler, @@ -140,6 +146,42 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { /* numOutputBuffers= */ 4); } + /** + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. + * @param threads Number of threads libvpx will use to decode. + * @param numInputBuffers Number of input buffers. + * @param numOutputBuffers Number of output buffers. + */ + @SuppressWarnings("deprecation") + public LibvpxVideoRenderer( + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify, + boolean enableRowMultiThreadMode, + int threads, + int numInputBuffers, + int numOutputBuffers) { + this( + allowedJoiningTimeMs, + eventHandler, + eventListener, + maxDroppedFramesToNotify, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableRowMultiThreadMode, + threads, + numInputBuffers, + numOutputBuffers); + } + /** * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. @@ -159,7 +201,11 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * @param threads Number of threads libvpx will use to decode. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. + * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, + * boolean, int, int, int)}} instead, and pass DRM-related parameters to the {@link + * MediaSource} factories. */ + @Deprecated public LibvpxVideoRenderer( long allowedJoiningTimeMs, @Nullable Handler eventHandler, 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 e362697179..873219d31d 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 @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; @@ -102,6 +103,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param context A context. * @param mediaCodecSelector A decoder selector. */ + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) { this( context, @@ -120,7 +122,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * 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. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -142,6 +149,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -169,7 +177,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @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. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -204,7 +217,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before * output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -238,7 +256,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * 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. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -258,6 +281,36 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media audioSink); } + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @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. + */ + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + audioSink); + } + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -275,7 +328,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * 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. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, 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 c04230e2b0..38ac80bf26 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 @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; @@ -199,6 +200,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ + @SuppressWarnings("deprecation") public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -234,7 +236,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -256,6 +263,41 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maxDroppedFramesToNotify); } + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + */ + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -276,7 +318,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, From 3023bb4d9185461673e8afb90fb2b08ee327c537 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 22 Nov 2019 15:35:18 +0000 Subject: [PATCH 750/807] Remove future release notes --- RELEASENOTES.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a9fbe01e6e..ac74931671 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,17 +1,5 @@ # Release notes # -### dev-v2 (not yet released) ### - -* Video tunneling: Fix renderer end-of-stream with `OnFrameRenderedListener` - from API 23, tunneled renderer must send a special timestamp on EOS. - Previously the EOS was reported when the input stream reached EOS. -* Require an end time or duration for SubRip (SRT) and SubStation Alpha - (SSA/ASS) subtitles. This applies to both sidecar files & subtitles - [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). -* Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid `ExoMediaDrm` - leaks ([#4721](https://github.com/google/ExoPlayer/issues/4721)). -* Fix byte order of HDR10+ static metadata to match CTA-861.3. - ### 2.11.0 (not yet released) ### * Core library: @@ -68,6 +56,7 @@ configuration of the audio capture policy. * Video: * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. + * Fix byte order of HDR10+ static metadata to match CTA-861.3. * Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. * Assume that protected content requires a secure decoder when evaluating whether `MediaCodecVideoRenderer` supports a given video format From 1730b6bb51c8e487b51a32a9e5936828de1cdadb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 19 Nov 2019 11:57:23 +0000 Subject: [PATCH 751/807] Reconfigure audio sink when PCM encoding changes Note: - Fixing this uncovers another bug in how audio processor draining works, so the test playlist still doesn't play correctly after this change. - Once we reconfigure the audio sink based on the ExoPlayer Format rather than the codec MediaFormat in a later change, this change can be reverted. Issue: #6601 PiperOrigin-RevId: 281264149 --- .../google/android/exoplayer2/audio/MediaCodecAudioRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 873219d31d..a6a8b03448 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 @@ -512,6 +512,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType) && oldFormat.channelCount == newFormat.channelCount && oldFormat.sampleRate == newFormat.sampleRate + && oldFormat.pcmEncoding == newFormat.pcmEncoding && oldFormat.initializationDataEquals(newFormat) && !MimeTypes.AUDIO_OPUS.equals(oldFormat.sampleMimeType); } From fc0aa08d794338f5a3cb284bd736ca8bb8d962ee Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 19 Nov 2019 18:01:03 +0000 Subject: [PATCH 752/807] Add testutils as test dep of library-core module The current workaround seems to cause compilation errors inside the testutils module in Android Studio. This seems to fix them. This doesn't introduce a circular dependency because it's only the tests in library-core depending on testutils. PiperOrigin-RevId: 281318192 --- library/core/build.gradle | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index e9cb6d5a18..e145a179d9 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -35,16 +35,6 @@ android { testInstrumentationRunnerArguments clearPackageData: 'true' } - // Workaround to prevent circular dependency on project :testutils. - sourceSets { - androidTest { - java.srcDirs += '../../testutils/src/main/java/' - } - test { - java.srcDirs += '../../testutils/src/main/java/' - } - } - buildTypes { debug { testCoverageEnabled = true @@ -66,11 +56,13 @@ dependencies { androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion + androidTestImplementation project(modulePrefix + 'testutils') testImplementation 'androidx.test:core:' + androidxTestCoreVersion testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion + testImplementation project(modulePrefix + 'testutils') } ext { From febfeca2c6ba76caaed816eaba48a998ea152cf5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 20 Nov 2019 11:58:57 +0000 Subject: [PATCH 753/807] Mark all methods accessing SQLite databases as potentially blocking. They are all marked with a JavaDoc comment and the @WorkerThread annotation which is useful if apps are using threading annotations. All other public methods in the same classes are marked with @AnyThread to avoid the impression we forgot to annotate them. PiperOrigin-RevId: 281490301 --- .../offline/ActionFileUpgradeUtil.java | 4 ++ .../exoplayer2/offline/DownloadIndex.java | 6 +++ .../offline/WritableDownloadIndex.java | 14 +++++++ .../exoplayer2/upstream/cache/Cache.java | 40 ++++++++++++++----- .../cache/CacheFileMetadataIndex.java | 19 +++++++++ .../exoplayer2/upstream/cache/CacheUtil.java | 13 ++++++ .../upstream/cache/CachedContentIndex.java | 10 +++++ .../upstream/cache/SimpleCache.java | 5 +++ 8 files changed, 102 insertions(+), 9 deletions(-) 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 9ecce6e150..999059e852 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline; import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; @@ -47,6 +48,8 @@ public final class ActionFileUpgradeUtil { *

        This method must not be called while the {@link DefaultDownloadIndex} is being used by a * {@link DownloadManager}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param actionFilePath The action file path. * @param downloadIdProvider A download ID provider, or {@code null}. If {@code null} then ID of * each download will be its custom cache key if one is specified, or else its URL. @@ -55,6 +58,7 @@ public final class ActionFileUpgradeUtil { * @param addNewDownloadsAsCompleted Whether to add new downloads as completed. * @throws IOException If an error occurs loading or merging the requests. */ + @WorkerThread @SuppressWarnings("deprecation") public static void upgradeAndDelete( File actionFilePath, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java index 3de1b7b212..e0ccd23c71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -16,14 +16,18 @@ package com.google.android.exoplayer2.offline; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import java.io.IOException; /** An index of {@link Download Downloads}. */ +@WorkerThread public interface DownloadIndex { /** * Returns the {@link Download} with the given {@code id}, or null. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param id ID of a {@link Download}. * @return The {@link Download} with the given {@code id}, or null if a download state with this * id doesn't exist. @@ -35,6 +39,8 @@ public interface DownloadIndex { /** * Returns a {@link DownloadCursor} to {@link Download}s with the given {@code states}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param states Returns only the {@link Download}s with this states. If empty, returns all. * @return A cursor to {@link Download}s with the given {@code states}. * @throws IOException If an error occurs reading the state. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index dc7085c85e..d49b4c39ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -15,14 +15,18 @@ */ package com.google.android.exoplayer2.offline; +import androidx.annotation.WorkerThread; import java.io.IOException; /** A writable index of {@link Download Downloads}. */ +@WorkerThread public interface WritableDownloadIndex extends DownloadIndex { /** * Adds or replaces a {@link Download}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param download The {@link Download} to be added. * @throws IOException If an error occurs setting the state. */ @@ -32,6 +36,8 @@ public interface WritableDownloadIndex extends DownloadIndex { * Removes the download with the given ID. Does nothing if a download with the given ID does not * exist. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param id The ID of the download to remove. * @throws IOException If an error occurs removing the state. */ @@ -40,6 +46,8 @@ public interface WritableDownloadIndex extends DownloadIndex { /** * Sets all {@link Download#STATE_DOWNLOADING} states to {@link Download#STATE_QUEUED}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @throws IOException If an error occurs updating the state. */ void setDownloadingStatesToQueued() throws IOException; @@ -47,6 +55,8 @@ public interface WritableDownloadIndex extends DownloadIndex { /** * Sets all states to {@link Download#STATE_REMOVING}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @throws IOException If an error occurs updating the state. */ void setStatesToRemoving() throws IOException; @@ -55,6 +65,8 @@ public interface WritableDownloadIndex extends DownloadIndex { * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param stopReason The stop reason. * @throws IOException If an error occurs updating the state. */ @@ -65,6 +77,8 @@ public interface WritableDownloadIndex extends DownloadIndex { * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). Does nothing if a download with the * given ID does not exist, or if it's not in a terminal state. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param id The ID of the download to update. * @param stopReason The stop reason. * @throws IOException If an error occurs updating the state. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 12905f908c..1d504159e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; @@ -100,7 +101,10 @@ public interface Cache { /** * Releases the cache. This method must be called when the cache is no longer required. The cache * must not be used after calling this method. + * + *

        This method may be slow and shouldn't normally be called on the main thread. */ + @WorkerThread void release(); /** @@ -162,23 +166,29 @@ public interface Cache { * calling {@link #commitFile(File, long)}. When the caller has finished writing, it must release * the lock by calling {@link #releaseHoleSpan}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. * @throws InterruptedException If the thread was interrupted. * @throws CacheException If an error is encountered. */ + @WorkerThread CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; /** * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then * instead of blocking, this method will return null as the {@link CacheSpan}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. * @throws CacheException If an error is encountered. */ + @WorkerThread @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; @@ -186,6 +196,8 @@ public interface Cache { * Obtains a cache file into which data can be written. Must only be called when holding a * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param key The cache key for the data. * @param position The starting position of the data. * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown. Used @@ -193,16 +205,20 @@ public interface Cache { * @return The file into which data should be written. * @throws CacheException If an error is encountered. */ + @WorkerThread File startFile(String key, long position, long length) throws CacheException; /** * Commits a file into the cache. Must only be called when holding a corresponding hole {@link - * CacheSpan} obtained from {@link #startReadWrite(String, long)} + * CacheSpan} obtained from {@link #startReadWrite(String, long)}. + * + *

        This method may be slow and shouldn't normally be called on the main thread. * * @param file A newly written cache file. * @param length The length of the newly written cache file in bytes. * @throws CacheException If an error is encountered. */ + @WorkerThread void commitFile(File file, long length) throws CacheException; /** @@ -216,19 +232,22 @@ public interface Cache { /** * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param span The {@link CacheSpan} to remove. * @throws CacheException If an error is encountered. */ + @WorkerThread void removeSpan(CacheSpan span) throws CacheException; - /** - * Queries if a range is entirely available in the cache. - * - * @param key The cache key for the data. - * @param position The starting position of the data. - * @param length The length of the data. - * @return true if the data is available in the Cache otherwise false; - */ + /** + * Queries if a range is entirely available in the cache. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The length of the data. + * @return true if the data is available in the Cache otherwise false; + */ boolean isCached(String key, long position, long length); /** @@ -247,10 +266,13 @@ public interface Cache { * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link * CachedContent} is added if there isn't one already with the given key. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param key The cache key for the data. * @param mutations Contains mutations to be applied to the metadata. * @throws CacheException If an error is encountered. */ + @WorkerThread void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) throws CacheException; 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 2488ae0ff3..978dbbc8f7 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 @@ -19,6 +19,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; @@ -64,10 +65,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Deletes index data for the specified cache. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param databaseProvider Provides the database in which the index is stored. * @param uid The cache UID. * @throws DatabaseIOException If an error occurs deleting the index data. */ + @WorkerThread public static void delete(DatabaseProvider databaseProvider, long uid) throws DatabaseIOException { String hexUid = Long.toHexString(uid); @@ -96,9 +100,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Initializes the index for the given cache UID. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param uid The cache UID. * @throws DatabaseIOException If an error occurs initializing the index. */ + @WorkerThread public void initialize(long uid) throws DatabaseIOException { try { String hexUid = Long.toHexString(uid); @@ -129,9 +136,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Returns all file metadata keyed by file name. The returned map is mutable and may be modified * by the caller. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @return The file metadata keyed by file name. * @throws DatabaseIOException If an error occurs loading the metadata. */ + @WorkerThread public Map getAll() throws DatabaseIOException { try (Cursor cursor = getCursor()) { Map fileMetadata = new HashMap<>(cursor.getCount()); @@ -150,11 +160,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Sets metadata for a given file. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param name The name of the file. * @param length The file length. * @param lastTouchTimestamp The file last touch timestamp. * @throws DatabaseIOException If an error occurs setting the metadata. */ + @WorkerThread public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { @@ -172,9 +185,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Removes metadata. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param name The name of the file whose metadata is to be removed. * @throws DatabaseIOException If an error occurs removing the metadata. */ + @WorkerThread public void remove(String name) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { @@ -188,9 +204,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Removes metadata. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param names The names of the files whose metadata is to be removed. * @throws DatabaseIOException If an error occurs removing the metadata. */ + @WorkerThread public void removeAll(Set names) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { 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 bbec189b4d..93b00718ab 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; @@ -104,6 +105,8 @@ public final class CacheUtil { * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early * if the end of the input is reached. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. * @param cacheKeyFactory An optional factory for cache keys. @@ -113,6 +116,7 @@ public final class CacheUtil { * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ + @WorkerThread public static void cache( DataSpec dataSpec, Cache cache, @@ -144,6 +148,8 @@ public final class CacheUtil { * PriorityTaskManager#add} to register with the manager before calling this method, and to call * {@link PriorityTaskManager#remove} afterwards to unregister. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. * @param cacheKeyFactory An optional factory for cache keys. @@ -159,6 +165,7 @@ public final class CacheUtil { * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ + @WorkerThread public static void cache( DataSpec dataSpec, Cache cache, @@ -333,10 +340,13 @@ public final class CacheUtil { /** * Removes all of the data specified by the {@code dataSpec}. * + *

        This methods blocks until the operation is complete. + * * @param dataSpec Defines the data to be removed. * @param cache A {@link Cache} to store the data. * @param cacheKeyFactory An optional factory for cache keys. */ + @WorkerThread public static void remove( DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { remove(cache, buildCacheKey(dataSpec, cacheKeyFactory)); @@ -345,9 +355,12 @@ public final class CacheUtil { /** * Removes all of the data specified by the {@code key}. * + *

        This methods blocks until the operation is complete. + * * @param cache A {@link Cache} to store the data. * @param key The key whose data should be removed. */ + @WorkerThread public static void remove(Cache cache, String key) { NavigableSet cachedSpans = cache.getCachedSpans(key); for (CacheSpan cachedSpan : cachedSpans) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 22086f8ac8..5ed3e921ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -25,6 +25,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; @@ -104,10 +105,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Deletes index data for the specified cache. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param databaseProvider Provides the database in which the index is stored. * @param uid The cache UID. * @throws DatabaseIOException If an error occurs deleting the index data. */ + @WorkerThread public static void delete(DatabaseProvider databaseProvider, long uid) throws DatabaseIOException { DatabaseStorage.delete(databaseProvider, uid); @@ -174,9 +178,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Loads the index data for the given cache UID. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param uid The UID of the cache whose index is to be loaded. * @throws IOException If an error occurs initializing the index data. */ + @WorkerThread public void initialize(long uid) throws IOException { storage.initialize(uid); if (previousStorage != null) { @@ -199,8 +206,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Stores the index data to index file if there is a change. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @throws IOException If an error occurs storing the index data. */ + @WorkerThread public void store() throws IOException { storage.storeIncremental(keyToContent); // Make ids that were removed since the index was last stored eligible for re-use. 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 cf8056f5a9..a4fade25e0 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; @@ -85,10 +86,13 @@ public final class SimpleCache implements Cache { /** * Deletes all content belonging to a cache instance. * + *

        This method may be slow and shouldn't normally be called on the main thread. + * * @param cacheDir The cache directory. * @param databaseProvider The database in which index data is stored, or {@code null} if the * cache used a legacy index. */ + @WorkerThread public static void delete(File cacheDir, @Nullable DatabaseProvider databaseProvider) { if (!cacheDir.exists()) { return; @@ -147,6 +151,7 @@ public final class SimpleCache implements Cache { * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance. */ @Deprecated + @SuppressWarnings("deprecation") public SimpleCache(File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey) { this(cacheDir, evictor, secretKey, secretKey != null); } From 704bd8af094bd73a9c4ea0b088ecadc9b16b00a9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Nov 2019 14:52:51 +0000 Subject: [PATCH 754/807] Remove stray word in logging PiperOrigin-RevId: 281510703 --- .../com/google/android/exoplayer2/drm/DefaultDrmSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0c36bc7d0f..6140acdff6 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 @@ -400,7 +400,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); return true; } catch (Exception e) { - Log.e(TAG, "Error trying to restore Widevine keys.", e); + Log.e(TAG, "Error trying to restore keys.", e); onError(e); } return false; From 8becf02c30722e68c02b7adaab4fda50e70f620c Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 20 Nov 2019 16:01:45 +0000 Subject: [PATCH 755/807] Replace all database.beginTransaction with beginTransactionNonExclusive This ensures other database readers can continue reading while we do our write transaction. PiperOrigin-RevId: 281520758 --- .../android/exoplayer2/offline/DefaultDownloadIndex.java | 2 +- .../exoplayer2/upstream/cache/CacheFileMetadataIndex.java | 6 +++--- .../exoplayer2/upstream/cache/CachedContentIndex.java | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 4517e4ee9a..7ed1eb095f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -285,7 +285,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.setVersion( writableDatabase, VersionTable.FEATURE_OFFLINE, name, TABLE_VERSION); 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 978dbbc8f7..dc27dec363 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 @@ -78,7 +78,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; try { String tableName = getTableName(hexUid); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.removeVersion( writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid); @@ -116,7 +116,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.setVersion( writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION); @@ -214,7 +214,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Assertions.checkNotNull(tableName); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { for (String name : names) { writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name}); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 5ed3e921ee..7e09025ddd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -797,7 +797,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; hexUid); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { initializeTable(writableDatabase); writableDatabase.setTransactionSuccessful(); @@ -832,7 +832,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void storeFully(HashMap content) throws IOException { try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { initializeTable(writableDatabase); for (CachedContent cachedContent : content.values()) { @@ -855,7 +855,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { for (int i = 0; i < pendingUpdates.size(); i++) { CachedContent cachedContent = pendingUpdates.valueAt(i); @@ -931,7 +931,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; try { String tableName = getTableName(hexUid); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.removeVersion( writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid); From b0eea31391392d6615096acce788caf677f68e65 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 21 Nov 2019 11:56:25 +0000 Subject: [PATCH 756/807] Fix broken Enc/Clear playlist in media.exolist.json - DRM properties need to be on individual playlist items. PiperOrigin-RevId: 281718153 --- demos/main/src/main/assets/media.exolist.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 57f0045791..01980c2f36 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -451,23 +451,27 @@ }, { "name": "Clear -> Enc -> Clear -> Enc -> Enc", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", "playlist": [ { "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" } ] } From 9be09b35e24cd2d9863d3e6c1cc1920bb257b937 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 21 Nov 2019 12:53:06 +0000 Subject: [PATCH 757/807] Add AV1 to supported formats PiperOrigin-RevId: 281724630 --- extensions/av1/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/av1/README.md b/extensions/av1/README.md index fb7bc4bde8..b57f0b484c 100644 --- a/extensions/av1/README.md +++ b/extensions/av1/README.md @@ -59,7 +59,7 @@ to configure and build libgav1 and the extension's [JNI wrapper library][]. [Install CMake]: https://developer.android.com/studio/projects/install-ndk [CMake]: https://cmake.org/ [Ninja]: https://ninja-build.org -[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/dev-v2/extensions/av1/src/main/jni/gav1_jni.cc +[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc ## Using the extension ## From cc520a670ed8d7e571c845bf6a1f1410a3e0bd39 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 21 Nov 2019 16:50:24 +0000 Subject: [PATCH 758/807] Simplify playback of clear samples without keys - Move property to DrmSession; it feels like a more natural place for it to go (and provides greater flexibility). - Change flags to a boolean. PiperOrigin-RevId: 281758729 --- .../exoplayer2/drm/DefaultDrmSession.java | 8 +++++ .../drm/DefaultDrmSessionManager.java | 27 +++++++---------- .../android/exoplayer2/drm/DrmSession.java | 5 ++++ .../exoplayer2/drm/DrmSessionManager.java | 30 ------------------- .../exoplayer2/drm/ErrorStateDrmSession.java | 5 ++++ .../source/SampleMetadataQueue.java | 9 ++---- .../exoplayer2/source/SampleQueueTest.java | 6 ++-- 7 files changed, 33 insertions(+), 57 deletions(-) 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 6140acdff6..a619674fa0 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 @@ -106,6 +106,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ProvisioningManager provisioningManager; private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; + private final boolean playClearSamplesWithoutKeys; private final boolean isPlaceholderSession; private final HashMap keyRequestParameters; private final EventDispatcher eventDispatcher; @@ -154,6 +155,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ReleaseCallback releaseCallback, @Nullable List schemeDatas, @DefaultDrmSessionManager.Mode int mode, + boolean playClearSamplesWithoutKeys, boolean isPlaceholderSession, @Nullable byte[] offlineLicenseKeySetId, HashMap keyRequestParameters, @@ -170,6 +172,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.releaseCallback = releaseCallback; this.mediaDrm = mediaDrm; this.mode = mode; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.isPlaceholderSession = isPlaceholderSession; if (offlineLicenseKeySetId != null) { this.offlineLicenseKeySetId = offlineLicenseKeySetId; @@ -228,6 +231,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return state; } + @Override + public boolean playClearSamplesWithoutKeys() { + return playClearSamplesWithoutKeys; + } + @Override public final @Nullable DrmSessionException getError() { return state == STATE_ERROR ? lastException : null; 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 f8e854e51d..1c27d745de 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 @@ -58,7 +58,7 @@ public class DefaultDrmSessionManager implements DrmSe private ExoMediaDrm.Provider exoMediaDrmProvider; private boolean multiSession; private int[] useDrmSessionsForClearContentTrackTypes; - @Flags private int flags; + private boolean playClearSamplesWithoutKeys; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; /** @@ -164,11 +164,7 @@ public class DefaultDrmSessionManager implements DrmSe * @return This builder. */ public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) { - if (playClearSamplesWithoutKeys) { - this.flags |= FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS; - } else { - this.flags &= ~FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS; - } + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; return this; } @@ -192,7 +188,7 @@ public class DefaultDrmSessionManager implements DrmSe keyRequestParameters, multiSession, useDrmSessionsForClearContentTrackTypes, - flags, + playClearSamplesWithoutKeys, loadErrorHandlingPolicy); } } @@ -245,7 +241,7 @@ public class DefaultDrmSessionManager implements DrmSe private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int[] useDrmSessionsForClearContentTrackTypes; - @Flags private final int flags; + private final boolean playClearSamplesWithoutKeys; private final ProvisioningManagerImpl provisioningManagerImpl; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -339,7 +335,7 @@ public class DefaultDrmSessionManager implements DrmSe keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, multiSession, /* useDrmSessionsForClearContentTrackTypes= */ new int[0], - /* flags= */ 0, + /* playClearSamplesWithoutKeys= */ false, new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } @@ -352,7 +348,7 @@ public class DefaultDrmSessionManager implements DrmSe HashMap keyRequestParameters, boolean multiSession, int[] useDrmSessionsForClearContentTrackTypes, - @Flags int flags, + boolean playClearSamplesWithoutKeys, LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkNotNull(uuid); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); @@ -363,7 +359,7 @@ public class DefaultDrmSessionManager implements DrmSe this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; - this.flags = flags; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; provisioningManagerImpl = new ProvisioningManagerImpl(); mode = MODE_PLAYBACK; @@ -541,12 +537,6 @@ public class DefaultDrmSessionManager implements DrmSe return session; } - @Override - @Flags - public final int getFlags() { - return flags; - } - @Override @Nullable public Class getExoMediaCryptoType(DrmInitData drmInitData) { @@ -571,6 +561,8 @@ public class DefaultDrmSessionManager implements DrmSe private DefaultDrmSession createNewDefaultSession( @Nullable List schemeDatas, boolean isPlaceholderSession) { Assertions.checkNotNull(exoMediaDrm); + // Placeholder sessions should always play clear samples without keys. + boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; return new DefaultDrmSession<>( uuid, exoMediaDrm, @@ -578,6 +570,7 @@ public class DefaultDrmSessionManager implements DrmSe /* releaseCallback= */ this::onSessionReleased, schemeDatas, mode, + playClearSamplesWithoutKeys, isPlaceholderSession, offlineLicenseKeySetId, keyRequestParameters, 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 13e29e141a..6c2fdecd01 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 @@ -93,6 +93,11 @@ public interface DrmSession { */ @State int getState(); + /** Returns whether this session allows playback of clear samples prior to keys being loaded. */ + default boolean playClearSamplesWithoutKeys() { + return false; + } + /** * Returns the cause of the error state, or null if {@link #getState()} is not {@link * #STATE_ERROR}. 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 c92d68ed17..4aef731558 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,9 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; 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. @@ -59,26 +55,6 @@ public interface DrmSessionManager { } }; - /** 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; - /** * Acquires any required resources. * @@ -136,12 +112,6 @@ public interface DrmSessionManager { */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); - /** 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}. 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 aa15c82900..0028e47987 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 @@ -33,6 +33,11 @@ public final class ErrorStateDrmSession implements Drm return STATE_ERROR; } + @Override + public boolean playClearSamplesWithoutKeys() { + return false; + } + @Override @Nullable public DrmSessionException getError() { 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 0cc576a145..22061f58ef 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 @@ -49,7 +49,6 @@ import java.io.IOException; private static final int SAMPLE_CAPACITY_INCREMENT = 1000; private final DrmSessionManager drmSessionManager; - private final boolean playClearSamplesWithoutKeys; @Nullable private Format downstreamFormat; @Nullable private DrmSession currentDrmSession; @@ -79,9 +78,6 @@ import java.io.IOException; public SampleMetadataQueue(DrmSessionManager drmSessionManager) { this.drmSessionManager = drmSessionManager; - playClearSamplesWithoutKeys = - (drmSessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) - != 0; capacity = SAMPLE_CAPACITY_INCREMENT; sourceIds = new int[capacity]; offsets = new long[capacity]; @@ -282,7 +278,7 @@ import java.io.IOException; } else { // A clear sample in an encrypted section may be read if playClearSamplesWithoutKeys is true. return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 - && playClearSamplesWithoutKeys; + && Assertions.checkNotNull(currentDrmSession).playClearSamplesWithoutKeys(); } } @@ -341,7 +337,8 @@ import java.io.IOException; boolean mayReadSample = skipDrmChecks || Util.castNonNull(downstreamFormat).drmInitData == null - || (playClearSamplesWithoutKeys && !isNextSampleEncrypted) + || (Assertions.checkNotNull(currentDrmSession).playClearSamplesWithoutKeys() + && !isNextSampleEncrypted) || Assertions.checkNotNull(currentDrmSession).getState() == DrmSession.STATE_OPENED_WITH_KEYS; if (!mayReadSample) { 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 df6ccc1f02..441ac9e05a 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 @@ -331,8 +331,7 @@ public final class SampleQueueTest { @Test public void testIsReadyReturnsTrueForClearSampleAndPlayClearSamplesWithoutKeysIsTrue() { - when(mockDrmSessionManager.getFlags()) - .thenReturn(DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS); + when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true); // We recreate the queue to ensure the mock DRM session manager flags are taken into account. sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); writeTestDataWithEncryptedSections(); @@ -458,8 +457,7 @@ public final class SampleQueueTest { @Test public void testAllowPlayClearSamplesWithoutKeysReadsClearSamples() { - when(mockDrmSessionManager.getFlags()) - .thenReturn(DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS); + when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true); // We recreate the queue to ensure the mock DRM session manager flags are taken into account. sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); From d82da93ec4e25e845b3c596fe32c489e5eb1c780 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 21 Nov 2019 17:15:43 +0000 Subject: [PATCH 759/807] Simplify checking whether a sample can be read PiperOrigin-RevId: 281763672 --- .../source/SampleMetadataQueue.java | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) 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 22061f58ef..bb578ddec7 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 @@ -266,20 +266,8 @@ import java.io.IOException; if (formats[relativeReadIndex] != downstreamFormat) { // A format can be read. return true; - } else if (Assertions.checkNotNull(downstreamFormat).drmInitData == null) { - // A sample from a clear section can be read. - return true; - } else if (drmSessionManager == DrmSessionManager.DUMMY - || Assertions.checkNotNull(currentDrmSession).getState() - == DrmSession.STATE_OPENED_WITH_KEYS) { - // TODO: Remove DUMMY DrmSessionManager check once renderers are migrated [Internal ref: - // b/122519809]. - return true; - } else { - // A clear sample in an encrypted section may be read if playClearSamplesWithoutKeys is true. - return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 - && Assertions.checkNotNull(currentDrmSession).playClearSamplesWithoutKeys(); } + return mayReadSample(relativeReadIndex); } /** @@ -328,20 +316,7 @@ import java.io.IOException; return C.RESULT_FORMAT_READ; } - // It's likely that the media source creation has not yet been migrated and the renderer can - // acquire the session for the sample. - // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. - boolean skipDrmChecks = drmSessionManager == DrmSessionManager.DUMMY; - boolean isNextSampleEncrypted = (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0; - - boolean mayReadSample = - skipDrmChecks - || Util.castNonNull(downstreamFormat).drmInitData == null - || (Assertions.checkNotNull(currentDrmSession).playClearSamplesWithoutKeys() - && !isNextSampleEncrypted) - || Assertions.checkNotNull(currentDrmSession).getState() - == DrmSession.STATE_OPENED_WITH_KEYS; - if (!mayReadSample) { + if (!mayReadSample(relativeReadIndex)) { return C.RESULT_NOTHING_READ; } @@ -630,6 +605,25 @@ import java.io.IOException; } } + /** + * Returns whether it's possible to read the next sample. + * + * @param relativeReadIndex The relative read index of the next sample. + * @return Whether it's possible to read the next sample. + */ + private boolean mayReadSample(int relativeReadIndex) { + if (drmSessionManager == DrmSessionManager.DUMMY) { + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + // For protected content it's likely that the DrmSessionManager is still being injected into + // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed. + return true; + } + return currentDrmSession == null + || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS + || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 + && currentDrmSession.playClearSamplesWithoutKeys()); + } + /** * Finds the sample in the specified range that's before or at the specified time. If {@code * keyframe} is {@code true} then the sample is additionally required to be a keyframe. From de641380dff07dc1d798d84c9a4f367e0c24ad7d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 21 Nov 2019 17:18:56 +0000 Subject: [PATCH 760/807] Improve WakeLock/AudioBecomingNoisy Javadoc PiperOrigin-RevId: 281764207 --- .../android/exoplayer2/SimpleExoPlayer.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) 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 729dd150ae..52b686000a 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 @@ -801,7 +801,10 @@ public class SimpleExoPlayer extends BasePlayer * href="https://developer.android.com/guide/topics/media-apps/volume-and-earphones#becoming-noisy">audio * becoming noisy documentation for more information. * - * @param handleAudioBecomingNoisy True if the player should handle audio becoming noisy. + *

        This feature is not enabled by default. + * + * @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is + * rerouted from a headset to device speakers. */ public void setHandleAudioBecomingNoisy(boolean handleAudioBecomingNoisy) { verifyApplicationThread(); @@ -1415,16 +1418,20 @@ public class SimpleExoPlayer extends BasePlayer } /** - * Sets whether to enable the acquiring and releasing of a {@link - * android.os.PowerManager.WakeLock}. + * Sets whether the player should use a {@link android.os.PowerManager.WakeLock} to ensure the + * device stays awake for playback, even when the screen is off. * - *

        By default, automatic wake lock handling is not enabled. Enabling this on will acquire the - * WakeLock if necessary. Disabling this will release the WakeLock if it is held. + *

        Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback can occur when the screen is off (e.g. background audio playback). It is not useful if + * the screen will always be on during playback (e.g. foreground video playback). * - * @param handleWakeLock True if the player should handle a {@link - * android.os.PowerManager.WakeLock}, false otherwise. This is for use with a foreground - * {@link android.app.Service}, for allowing audio playback with the screen off. Please note - * that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + *

        This feature is not enabled by default. If enabled, a WakeLock is held whenever the player + * is in the {@link #STATE_READY READY} or {@link #STATE_BUFFERING BUFFERING} states with {@code + * playWhenReady = true}. + * + * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} + * to ensure the device stays awake for playback, even when the screen is off. */ public void setHandleWakeLock(boolean handleWakeLock) { wakeLockManager.setEnabled(handleWakeLock); From ff19262a5e33203cac08f08abeb962cf48d8e4f1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Nov 2019 13:57:43 +0000 Subject: [PATCH 761/807] Make placeholder sessions report their true state Note that the renderer changes will all disappear when we remove legacy injection of DrmSessionManager into renderers. PiperOrigin-RevId: 281952601 --- .../exoplayer2/audio/SimpleDecoderAudioRenderer.java | 4 +++- .../google/android/exoplayer2/drm/DefaultDrmSession.java | 1 - .../com/google/android/exoplayer2/drm/DrmSession.java | 8 ++------ .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 +++- .../exoplayer2/video/SimpleDecoderVideoRenderer.java | 4 +++- 5 files changed, 11 insertions(+), 10 deletions(-) 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 76abfd6e0f..21991008cb 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 @@ -473,7 +473,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (decoderDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { return false; } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); 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 a619674fa0..0d93ec7c62 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 @@ -356,7 +356,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull("sessionId") private void doLicense(boolean allowRetry) { if (isPlaceholderSession) { - state = STATE_OPENED_WITH_KEYS; return; } byte[] sessionId = Util.castNonNull(this.sessionId); 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 6c2fdecd01..35358f04f7 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 @@ -77,13 +77,9 @@ public interface DrmSession { * The session is being opened. */ int STATE_OPENING = 2; - /** - * The session is open, but does not yet have the keys required for decryption. - */ + /** The session is open, but does not have keys required for decryption. */ int STATE_OPENED = 3; - /** - * The session is open and has the keys required for decryption. - */ + /** The session is open and has keys required for decryption. */ int STATE_OPENED_WITH_KEYS = 4; /** 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 54e0904dab..1361bb6ad4 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 @@ -1191,7 +1191,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (codecDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) { return false; } @DrmSession.State int drmSessionState = codecDrmSession.getState(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 86181664ba..73c964d1fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -878,7 +878,9 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (decoderDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { return false; } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); From 25b6a02026f83d4739d441c5df7dabd6dbd31804 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Nov 2019 16:22:40 +0000 Subject: [PATCH 762/807] Fix check for E-AC3 JOC in DASH Issue: #6636 PiperOrigin-RevId: 281972403 --- .../exoplayer2/source/dash/manifest/DashManifestParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ff00c5b0d4..b107be4794 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 @@ -1477,7 +1477,7 @@ public class DashManifestParser extends DefaultHandler for (int i = 0; i < supplementalProperties.size(); i++) { Descriptor descriptor = supplementalProperties.get(i); String schemeIdUri = descriptor.schemeIdUri; - if (("tag:dolby.com,2018:dash:EC3_ExtensionComplexityIndex:2018".equals(schemeIdUri) + if (("tag:dolby.com,2018:dash:EC3_ExtensionType:2018".equals(schemeIdUri) && "JOC".equals(descriptor.value)) || ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) && "ec+3".equals(descriptor.value))) { From bd0fbd0c89178098a33e1019d8c2aad6c0db9379 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Nov 2019 16:46:39 +0000 Subject: [PATCH 763/807] Fix incorrect Javadoc PiperOrigin-RevId: 281976465 --- .../google/android/exoplayer2/drm/DrmSessionManager.java | 4 ++-- .../com/google/android/exoplayer2/source/SampleQueue.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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 4aef731558..146c5d704d 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 @@ -86,8 +86,8 @@ public interface DrmSessionManager { * DrmSession#release()} to decrement the reference count. * *

        Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for - * playback of clear samples, which reduces the costs of transitioning between clear and encrypted - * content periods. + * playback of clear content periods. This can reduce the cost of transitioning between clear and + * encrypted content periods. * * @param playbackLooper The looper associated with the media playback thread. * @param trackType The type of the track to acquire a placeholder session for. Must be one of the 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 d92dd48d4e..1230b45fe4 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 @@ -332,13 +332,13 @@ public class SampleQueue implements TrackOutput { /** * Attempts to read from the queue. * - *

        {@link Format Formats} read from the this method may be associated to a {@link DrmSession} + *

        {@link Format Formats} read from this method may be associated to a {@link DrmSession} * through {@link FormatHolder#drmSession}, which is populated in two scenarios: * *

          - *
        • The sample has a {@link Format} with a non-null {@link Format#drmInitData}. - *
        • The {@link DrmSessionManager} is configured to use secure decoders for clear samples. See - * {@link DrmSessionManager#FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS}. + *
        • The {@link Format} has a non-null {@link Format#drmInitData}. + *
        • The {@link DrmSessionManager} provides placeholder sessions for this queue's track type. + * See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}. *
        * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. From e899e0ea81d4d155970b4b72ba7eb814c575f936 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 24 Nov 2019 15:53:08 +0000 Subject: [PATCH 764/807] Clean up 2.11.0 release notes PiperOrigin-RevId: 282227866 --- RELEASENOTES.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ac74931671..c175e7f240 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -30,15 +30,25 @@ * Fix issue where player errors are thrown too early at playlist transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). * DRM: - * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers` + * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. + This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a + different `DrmSessionManager` ([#5619](https://github.com/google/ExoPlayer/issues/5619)). - * Add a `DefaultDrmSessionManager.Builder`. - * Add support for the use of secure decoders in clear sections of content - ([#4867](https://github.com/google/ExoPlayer/issues/4867)). + * Add `DefaultDrmSessionManager.Builder`, and remove + `DefaultDrmSessionManager` static factory methods that leaked + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). + * Add support for the use of secure decoders when playing clear content + ([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can + be enabled using `DefaultDrmSessionManager.Builder`'s + `setUseDrmSessionsForClearContent` method. * Add support for custom `LoadErrorHandlingPolicies` in key and provisioning - requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). - * Remove `DefaultDrmSessionManager` factory methods that leak `ExoMediaDrm` - instances ([#4721](https://github.com/google/ExoPlayer/issues/4721)). + requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom + policies can be passed via `DefaultDrmSessionManager.Builder`'s + `setLoadErrorHandlingPolicy` method. + * Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking + `ExoMediaDrm` instances + ([#4721](https://github.com/google/ExoPlayer/issues/4721)). * Track selection: * Update `DefaultTrackSelector` to set a viewport constraint for the default display by default. @@ -57,11 +67,13 @@ * Video: * Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`. * Fix byte order of HDR10+ static metadata to match CTA-861.3. - * Support out-of-band HDR10+ metadata for VP9 in WebM/Matroska. + * Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska. * Assume that protected content requires a secure decoder when evaluating whether `MediaCodecVideoRenderer` supports a given video format ([#5568](https://github.com/google/ExoPlayer/issues/5568)). * Fix Dolby Vision fallback to AVC and HEVC. + * Fix early end-of-stream detection when using video tunneling, on API level + 23 and above. * Audio: * Fix the start of audio getting truncated when transitioning to a new item in a playlist of Opus streams. From a2b3ad863dc8c9ada12f2e1dc4d6a84cfec27819 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 26 Nov 2019 09:04:15 +0000 Subject: [PATCH 765/807] Always drain/flush AudioProcessors after configuration This simplifies the contract of configure and is in preparation for fixing a bug where more input can't be queued when draining audio processors for a configuration change. Issue: #6601 PiperOrigin-RevId: 282514367 --- .../exoplayer2/ext/gvr/GvrAudioProcessor.java | 7 +- .../exoplayer2/audio/AudioProcessor.java | 10 +-- .../exoplayer2/audio/BaseAudioProcessor.java | 10 +-- .../audio/ChannelMappingAudioProcessor.java | 11 +-- .../exoplayer2/audio/DefaultAudioSink.java | 11 +-- .../audio/FloatResamplingAudioProcessor.java | 4 +- .../audio/ResamplingAudioProcessor.java | 4 +- .../audio/SilenceSkippingAudioProcessor.java | 4 +- .../exoplayer2/audio/SonicAudioProcessor.java | 7 +- .../exoplayer2/audio/TeeAudioProcessor.java | 4 +- .../audio/TrimmingAudioProcessor.java | 4 +- .../SilenceSkippingAudioProcessorTest.java | 88 ++++--------------- 12 files changed, 38 insertions(+), 126 deletions(-) diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index 412eb58ad3..a6a6577831 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -87,16 +87,12 @@ public final class GvrAudioProcessor implements AudioProcessor { @SuppressWarnings("ReferenceEquality") @Override - public synchronized boolean configure( - int sampleRateHz, int channelCount, @C.Encoding int encoding) + public synchronized void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) throws UnhandledFormatException { if (encoding != C.ENCODING_PCM_16BIT) { maybeReleaseGvrAudioSurround(); throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { - return false; - } this.sampleRateHz = sampleRateHz; this.channelCount = channelCount; switch (channelCount) { @@ -125,7 +121,6 @@ public final class GvrAudioProcessor implements AudioProcessor { buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) .order(ByteOrder.nativeOrder()); } - return true; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index 1bf141cb43..b4f5040be5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -55,18 +55,16 @@ public interface AudioProcessor { *

        If the audio processor is active after configuration, call {@link #getOutputSampleRateHz()}, * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} to get its new output format. * - *

        If this method returns {@code true}, it is necessary to {@link #flush()} the processor - * before queueing more data, but you can (optionally) first drain output in the previous - * configuration by calling {@link #queueEndOfStream()} and {@link #getOutput()}. If this method - * returns {@code false}, it is safe to queue new input immediately. + *

        After calling this method, it is necessary to {@link #flush()} the processor to apply the + * new configuration before queueing more data. You can (optionally) first drain output in the + * previous configuration by calling {@link #queueEndOfStream()} and {@link #getOutput()}. * * @param sampleRateHz The sample rate of input audio in Hz. * @param channelCount The number of interleaved channels in input audio. * @param encoding The encoding of input audio. - * @return Whether the processor must be {@link #flush() flushed} before queueing more input. * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. */ - boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) + void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) throws UnhandledFormatException; /** Returns whether the processor is configured and will process input buffers. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java index a3a85bb43a..8fcf39367b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java @@ -104,18 +104,12 @@ public abstract class BaseAudioProcessor implements AudioProcessor { onReset(); } - /** Sets the input format of this processor, returning whether the input format has changed. */ - protected final boolean setInputFormat( + /** Sets the input format of this processor. */ + protected final void setInputFormat( int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - if (sampleRateHz == this.sampleRateHz - && channelCount == this.channelCount - && encoding == this.encoding) { - return false; - } this.sampleRateHz = sampleRateHz; this.channelCount = channelCount; this.encoding = encoding; - return true; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 6b84662093..8aea8506e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -19,7 +19,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; -import java.util.Arrays; /** * An {@link AudioProcessor} that applies a mapping from input channels onto specified output @@ -48,23 +47,20 @@ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { } @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) + public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) throws UnhandledFormatException { - boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); outputChannels = pendingOutputChannels; int[] outputChannels = this.outputChannels; if (outputChannels == null) { active = false; - return outputChannelsChanged; + return; } if (encoding != C.ENCODING_PCM_16BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - if (!outputChannelsChanged && !setInputFormat(sampleRateHz, channelCount, encoding)) { - return false; - } + setInputFormat(sampleRateHz, channelCount, encoding); active = channelCount != outputChannels.length; for (int i = 0; i < outputChannels.length; i++) { int channelIndex = outputChannels[i]; @@ -73,7 +69,6 @@ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { } active |= (channelIndex != i); } - return true; } @Override 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 65d997396b..7cdd700058 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 @@ -246,7 +246,7 @@ public final class DefaultAudioSink implements AudioSink { private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; - /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */ + /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ @Nullable private AudioTrack keepSessionIdAudioTrack; @Nullable private Configuration pendingConfiguration; @@ -432,13 +432,12 @@ public final class DefaultAudioSink implements AudioSink { shouldConvertHighResIntPcmToFloat ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; - boolean flushAudioProcessors = false; if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - flushAudioProcessors |= audioProcessor.configure(sampleRate, channelCount, encoding); + audioProcessor.configure(sampleRate, channelCount, encoding); } catch (AudioProcessor.UnhandledFormatException e) { throw new ConfigurationException(e); } @@ -473,11 +472,7 @@ public final class DefaultAudioSink implements AudioSink { processingEnabled, canApplyPlaybackParameters, availableAudioProcessors); - // If we have a pending configuration already, we always drain audio processors as the preceding - // configuration may have required it (even if this one doesn't). - boolean drainAudioProcessors = flushAudioProcessors || this.pendingConfiguration != null; - if (isInitialized() - && (!pendingConfiguration.canReuseAudioTrack(configuration) || drainAudioProcessors)) { + if (isInitialized()) { this.pendingConfiguration = pendingConfiguration; } else { configuration = pendingConfiguration; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index 2274d53b55..1e6af46fbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -29,12 +29,12 @@ import java.nio.ByteBuffer; private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF; @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) + public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) throws UnhandledFormatException { if (!Util.isEncodingHighResolutionIntegerPcm(encoding)) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - return setInputFormat(sampleRateHz, channelCount, encoding); + setInputFormat(sampleRateHz, channelCount, encoding); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index d0c057b676..4817504933 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -26,13 +26,13 @@ import java.nio.ByteBuffer; /* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) + public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) throws UnhandledFormatException { if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - return setInputFormat(sampleRateHz, channelCount, encoding); + setInputFormat(sampleRateHz, channelCount, encoding); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index caf8a61651..35e8f8aa5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -119,13 +119,13 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { // AudioProcessor implementation. @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) + public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) throws UnhandledFormatException { if (encoding != C.ENCODING_PCM_16BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } bytesPerFrame = channelCount * 2; - return setInputFormat(sampleRateHz, channelCount, encoding); + setInputFormat(sampleRateHz, channelCount, encoding); } @Override 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 bd32e5ee6f..5bffc8fc68 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 @@ -159,22 +159,17 @@ public final class SonicAudioProcessor implements AudioProcessor { } @Override - public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) + public void configure(int sampleRateHz, int channelCount, @Encoding int encoding) throws UnhandledFormatException { if (encoding != C.ENCODING_PCM_16BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } int outputSampleRateHz = pendingOutputSampleRateHz == SAMPLE_RATE_NO_CHANGE ? sampleRateHz : pendingOutputSampleRateHz; - if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount - && this.outputSampleRateHz == outputSampleRateHz) { - return false; - } this.sampleRateHz = sampleRateHz; this.channelCount = channelCount; this.outputSampleRateHz = outputSampleRateHz; pendingSonicRecreation = true; - return true; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java index 6e4c97701a..49b17916dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -64,8 +64,8 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - return setInputFormat(sampleRateHz, channelCount, encoding); + public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { + setInputFormat(sampleRateHz, channelCount, encoding); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index c9e9f921c7..161b7eb652 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -68,7 +68,7 @@ import java.nio.ByteBuffer; } @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) + public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) throws UnhandledFormatException { if (encoding != OUTPUT_ENCODING) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); @@ -80,11 +80,9 @@ import java.nio.ByteBuffer; endBuffer = new byte[trimEndFrames * bytesPerFrame]; endBufferSize = 0; pendingTrimStartBytes = trimStartFrames * bytesPerFrame; - boolean wasActive = isActive; isActive = trimStartFrames != 0 || trimEndFrames != 0; receivedInputSinceConfigure = false; setInputFormat(sampleRateHz, channelCount, encoding); - return wasActive != isActive; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java index 128591124d..854975e654 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledFormatException; import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -53,13 +52,10 @@ public final class SilenceSkippingAudioProcessorTest { silenceSkippingAudioProcessor.setEnabled(true); // When configuring it. - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); - silenceSkippingAudioProcessor.flush(); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); // It's active. - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); } @@ -87,47 +83,6 @@ public final class SilenceSkippingAudioProcessorTest { assertThat(silenceSkippingAudioProcessor.isActive()).isFalse(); } - @Test - public void testChangingSampleRate_requiresReconfiguration() throws Exception { - // Given an enabled processor and configured processor. - silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); - if (reconfigured) { - silenceSkippingAudioProcessor.flush(); - } - - // When reconfiguring it with a different sample rate. - reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ * 2, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); - - // It's reconfigured. - assertThat(reconfigured).isTrue(); - assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); - } - - @Test - public void testReconfiguringWithSameSampleRate_doesNotRequireReconfiguration() throws Exception { - // Given an enabled processor and configured processor. - silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); - assertThat(reconfigured).isTrue(); - silenceSkippingAudioProcessor.flush(); - - // When reconfiguring it with the same sample rate. - reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); - - // It's not reconfigured but it is active. - assertThat(reconfigured).isFalse(); - assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); - } - @Test public void testSkipInSilentSignal_skipsEverything() throws Exception { // Given a signal with only noise. @@ -141,11 +96,9 @@ public final class SilenceSkippingAudioProcessorTest { // When processing the entire signal. silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); silenceSkippingAudioProcessor.flush(); - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); @@ -170,11 +123,9 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); silenceSkippingAudioProcessor.flush(); - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); @@ -200,11 +151,9 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); silenceSkippingAudioProcessor.flush(); - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); @@ -230,11 +179,9 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); silenceSkippingAudioProcessor.flush(); - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 80); @@ -260,11 +207,9 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); silenceSkippingAudioProcessor.flush(); - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120); @@ -289,11 +234,9 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - boolean reconfigured = - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure( + TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); silenceSkippingAudioProcessor.flush(); - assertThat(reconfigured).isTrue(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); silenceSkippingAudioProcessor.flush(); @@ -309,8 +252,7 @@ public final class SilenceSkippingAudioProcessorTest { private static long process( SilenceSkippingAudioProcessor processor, InputBufferProvider inputBufferProvider, - int inputBufferSize) - throws UnhandledFormatException { + int inputBufferSize) { processor.flush(); long totalOutputFrames = 0; while (inputBufferProvider.hasRemaining()) { From 07b3cbc361fe8dbe186475032a1e0ede82769451 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 26 Nov 2019 09:08:59 +0000 Subject: [PATCH 766/807] Add AudioProcessor.AudioFormat Issue: #6601 PiperOrigin-RevId: 282515179 --- .../exoplayer2/ext/gvr/GvrAudioProcessor.java | 46 +++------ .../exoplayer2/audio/AudioProcessor.java | 96 +++++++++++-------- .../exoplayer2/audio/BaseAudioProcessor.java | 63 +++++------- .../audio/ChannelMappingAudioProcessor.java | 47 ++++----- .../exoplayer2/audio/DefaultAudioSink.java | 15 ++- .../audio/FloatResamplingAudioProcessor.java | 29 +++--- .../audio/ResamplingAudioProcessor.java | 26 ++--- .../audio/SilenceSkippingAudioProcessor.java | 18 ++-- .../exoplayer2/audio/SonicAudioProcessor.java | 86 ++++++++--------- .../exoplayer2/audio/TeeAudioProcessor.java | 8 +- .../audio/TrimmingAudioProcessor.java | 23 ++--- .../SilenceSkippingAudioProcessorTest.java | 58 ++++------- .../audio/SonicAudioProcessorTest.java | 81 ++++++++-------- 13 files changed, 263 insertions(+), 333 deletions(-) diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index a6a6577831..7748e053b4 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.gvr; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.util.Assertions; import com.google.vr.sdk.audio.GvrAudioSurround; @@ -44,8 +43,7 @@ public final class GvrAudioProcessor implements AudioProcessor { private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID; - private int sampleRateHz; - private int channelCount; + private AudioFormat inputAudioFormat; private int pendingGvrAudioSurroundFormat; @Nullable private GvrAudioSurround gvrAudioSurround; private ByteBuffer buffer; @@ -60,8 +58,7 @@ public final class GvrAudioProcessor implements AudioProcessor { public GvrAudioProcessor() { // Use the identity for the initial orientation. w = 1f; - sampleRateHz = Format.NO_VALUE; - channelCount = Format.NO_VALUE; + inputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; } @@ -87,15 +84,13 @@ public final class GvrAudioProcessor implements AudioProcessor { @SuppressWarnings("ReferenceEquality") @Override - public synchronized void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_16BIT) { + public synchronized AudioFormat configure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { maybeReleaseGvrAudioSurround(); - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + throw new UnhandledAudioFormatException(inputAudioFormat); } - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - switch (channelCount) { + switch (inputAudioFormat.channelCount) { case 1: pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO; break; @@ -115,12 +110,14 @@ public final class GvrAudioProcessor implements AudioProcessor { pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS; break; default: - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + throw new UnhandledAudioFormatException(inputAudioFormat); } if (buffer == EMPTY_BUFFER) { buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) .order(ByteOrder.nativeOrder()); } + this.inputAudioFormat = inputAudioFormat; + return new AudioFormat(inputAudioFormat.sampleRate, OUTPUT_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); } @Override @@ -128,21 +125,6 @@ public final class GvrAudioProcessor implements AudioProcessor { return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null; } - @Override - public int getOutputChannelCount() { - return OUTPUT_CHANNEL_COUNT; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; - } - - @Override - public int getOutputSampleRateHz() { - return sampleRateHz; - } - @Override public void queueInput(ByteBuffer input) { int position = input.position(); @@ -181,7 +163,10 @@ public final class GvrAudioProcessor implements AudioProcessor { maybeReleaseGvrAudioSurround(); gvrAudioSurround = new GvrAudioSurround( - pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER); + pendingGvrAudioSurroundFormat, + inputAudioFormat.sampleRate, + inputAudioFormat.channelCount, + FRAMES_PER_OUTPUT_BUFFER); gvrAudioSurround.updateNativeOrientation(w, x, y, z); pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; } else if (gvrAudioSurround != null) { @@ -195,8 +180,7 @@ public final class GvrAudioProcessor implements AudioProcessor { maybeReleaseGvrAudioSurround(); updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f); inputEnded = false; - sampleRateHz = Format.NO_VALUE; - channelCount = Format.NO_VALUE; + inputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index b4f5040be5..1b0ddff16c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -23,24 +25,56 @@ import java.nio.ByteOrder; * Interface for audio processors, which take audio data as input and transform it, potentially * modifying its channel count, encoding and/or sample rate. * - *

        Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then - * call {@link #isActive()} to determine whether the processor is active in the new configuration. - * {@link #queueInput(ByteBuffer)}, {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} - * and {@link #getOutputSampleRateHz()} may only be called if the processor is active. Call {@link - * #reset()} to reset the processor to its unconfigured state and release any resources. - * *

        In addition to being able to modify the format of audio, implementations may allow parameters * to be set that affect the output audio and whether the processor is active/inactive. */ public interface AudioProcessor { - /** Exception thrown when a processor can't be configured for a given input audio format. */ - final class UnhandledFormatException extends Exception { + /** PCM audio format that may be handled by an audio processor. */ + final class AudioFormat { + public static final AudioFormat NOT_SET = + new AudioFormat( + /* sampleRate= */ Format.NO_VALUE, + /* channelCount= */ Format.NO_VALUE, + /* encoding= */ Format.NO_VALUE); - public UnhandledFormatException( - int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " - + encoding); + /** The sample rate in Hertz. */ + public final int sampleRate; + /** The number of interleaved channels. */ + public final int channelCount; + /** The type of linear PCM encoding. */ + @C.PcmEncoding public final int encoding; + /** The number of bytes used to represent one audio frame. */ + public final int bytesPerFrame; + + public AudioFormat(int sampleRate, int channelCount, @C.PcmEncoding int encoding) { + this.sampleRate = sampleRate; + this.channelCount = channelCount; + this.encoding = encoding; + bytesPerFrame = + Util.isEncodingLinearPcm(encoding) + ? Util.getPcmFrameSize(encoding, channelCount) + : Format.NO_VALUE; + } + + @Override + public String toString() { + return "AudioFormat[" + + "sampleRate=" + + sampleRate + + ", channelCount=" + + channelCount + + ", encoding=" + + encoding + + ']'; + } + } + + /** Exception thrown when a processor can't be configured for a given input audio format. */ + final class UnhandledAudioFormatException extends Exception { + + public UnhandledAudioFormatException(AudioFormat inputAudioFormat) { + super("Unhandled format: " + inputAudioFormat); } } @@ -50,45 +84,23 @@ public interface AudioProcessor { /** * Configures the processor to process input audio with the specified format. After calling this - * method, call {@link #isActive()} to determine whether the audio processor is active. - * - *

        If the audio processor is active after configuration, call {@link #getOutputSampleRateHz()}, - * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} to get its new output format. + * method, call {@link #isActive()} to determine whether the audio processor is active. Returns + * the configured output audio format if this instance is active. * *

        After calling this method, it is necessary to {@link #flush()} the processor to apply the * new configuration before queueing more data. You can (optionally) first drain output in the * previous configuration by calling {@link #queueEndOfStream()} and {@link #getOutput()}. * - * @param sampleRateHz The sample rate of input audio in Hz. - * @param channelCount The number of interleaved channels in input audio. - * @param encoding The encoding of input audio. - * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + * @param inputAudioFormat The format of audio that will be queued after the next call to {@link + * #flush()}. + * @return The configured output audio format if this instance is {@link #isActive() active}. + * @throws UnhandledAudioFormatException Thrown if the specified format can't be handled as input. */ - void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException; + AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException; /** Returns whether the processor is configured and will process input buffers. */ boolean isActive(); - /** - * Returns the number of audio channels in the data output by the processor. The value may change - * as a result of calling {@link #configure(int, int, int)}. - */ - int getOutputChannelCount(); - - /** - * Returns the audio encoding used in the data output by the processor. The value may change as a - * result of calling {@link #configure(int, int, int)}. - */ - @C.PcmEncoding - int getOutputEncoding(); - - /** - * Returns the sample rate of audio output by the processor, in hertz. The value may change as a - * result of calling {@link #configure(int, int, int)}. - */ - int getOutputSampleRateHz(); - /** * Queues audio data between the position and limit of the input {@code buffer} for processing. * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as @@ -130,6 +142,6 @@ public interface AudioProcessor { */ void flush(); - /** Resets the processor to its unconfigured state. */ + /** Resets the processor to its unconfigured state, releasing any resources. */ void reset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java index 8fcf39367b..c9e5465644 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java @@ -16,24 +16,20 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.CallSuper; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Base class for audio processors that keep an output buffer and an internal buffer that is reused - * whenever input is queued. + * whenever input is queued. Subclasses should override {@link #onConfigure(AudioFormat)} to return + * the output audio format for the processor if it's active. */ public abstract class BaseAudioProcessor implements AudioProcessor { - /** The configured input sample rate, in Hertz, or {@link Format#NO_VALUE} if not configured. */ - protected int sampleRateHz; - /** The configured input channel count, or {@link Format#NO_VALUE} if not configured. */ - protected int channelCount; - /** The configured input encoding, or {@link Format#NO_VALUE} if not configured. */ - @C.PcmEncoding protected int encoding; + /** The configured input audio format. */ + protected AudioFormat inputAudioFormat; + private AudioFormat outputAudioFormat; private ByteBuffer buffer; private ByteBuffer outputBuffer; private boolean inputEnded; @@ -41,29 +37,21 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public BaseAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - encoding = Format.NO_VALUE; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; + } + + @Override + public final AudioFormat configure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + this.inputAudioFormat = inputAudioFormat; + outputAudioFormat = onConfigure(inputAudioFormat); + return isActive() ? outputAudioFormat : AudioFormat.NOT_SET; } @Override public boolean isActive() { - return sampleRateHz != Format.NO_VALUE; - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return encoding; - } - - @Override - public int getOutputSampleRateHz() { - return sampleRateHz; + return outputAudioFormat != AudioFormat.NOT_SET; } @Override @@ -98,20 +86,11 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public final void reset() { flush(); buffer = EMPTY_BUFFER; - sampleRateHz = Format.NO_VALUE; - channelCount = Format.NO_VALUE; - encoding = Format.NO_VALUE; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; onReset(); } - /** Sets the input format of this processor. */ - protected final void setInputFormat( - int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - this.encoding = encoding; - } - /** * Replaces the current output buffer with a buffer of at least {@code count} bytes and returns * it. Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be @@ -132,6 +111,12 @@ public abstract class BaseAudioProcessor implements AudioProcessor { return outputBuffer.hasRemaining(); } + /** Called when the processor is configured for a new input format. */ + protected AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + return AudioFormat.NOT_SET; + } + /** Called when the end-of-stream is queued to the processor. */ protected void onQueueEndOfStream() { // Do nothing. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 8aea8506e4..87be1ee88a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -24,19 +24,17 @@ import java.nio.ByteBuffer; * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ -/* package */ // the constructor does not initialize fields: pendingOutputChannels, outputChannels @SuppressWarnings("nullness:initialization.fields.uninitialized") -final class ChannelMappingAudioProcessor extends BaseAudioProcessor { +/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { @Nullable private int[] pendingOutputChannels; - private boolean active; @Nullable private int[] outputChannels; /** - * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} - * to start using the new channel map. + * Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to + * start using the new channel map. * * @param outputChannels The mapping from input to output channel indices, or {@code null} to * leave the input unchanged. @@ -47,38 +45,30 @@ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { } @Override - public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { outputChannels = pendingOutputChannels; int[] outputChannels = this.outputChannels; if (outputChannels == null) { - active = false; - return; - } - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + return AudioFormat.NOT_SET; } - setInputFormat(sampleRateHz, channelCount, encoding); - active = channelCount != outputChannels.length; + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); + } + + boolean active = inputAudioFormat.channelCount != outputChannels.length; for (int i = 0; i < outputChannels.length; i++) { int channelIndex = outputChannels[i]; - if (channelIndex >= channelCount) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + if (channelIndex >= inputAudioFormat.channelCount) { + throw new UnhandledAudioFormatException(inputAudioFormat); } active |= (channelIndex != i); } - } - - @Override - public boolean isActive() { - return active; - } - - @Override - public int getOutputChannelCount() { - return outputChannels == null ? channelCount : outputChannels.length; + return active + ? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT) + : AudioFormat.NOT_SET; } @Override @@ -86,14 +76,14 @@ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { int[] outputChannels = Assertions.checkNotNull(this.outputChannels); int position = inputBuffer.position(); int limit = inputBuffer.limit(); - int frameCount = (limit - position) / (2 * channelCount); + int frameCount = (limit - position) / (2 * inputAudioFormat.channelCount); int outputSize = frameCount * outputChannels.length * 2; ByteBuffer buffer = replaceOutputBuffer(outputSize); while (position < limit) { for (int channelIndex : outputChannels) { buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); } - position += channelCount * 2; + position += inputAudioFormat.channelCount * 2; } inputBuffer.position(limit); buffer.flip(); @@ -103,7 +93,6 @@ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { protected void onReset() { outputChannels = null; pendingOutputChannels = null; - active = false; } } 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 7cdd700058..27823e3006 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 @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -435,18 +436,22 @@ public final class DefaultAudioSink implements AudioSink { if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); + AudioProcessor.AudioFormat inputAudioFormat = + new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); + AudioProcessor.AudioFormat outputAudioFormat = inputAudioFormat; for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - audioProcessor.configure(sampleRate, channelCount, encoding); - } catch (AudioProcessor.UnhandledFormatException e) { + outputAudioFormat = audioProcessor.configure(inputAudioFormat); + } catch (UnhandledAudioFormatException e) { throw new ConfigurationException(e); } if (audioProcessor.isActive()) { - channelCount = audioProcessor.getOutputChannelCount(); - sampleRate = audioProcessor.getOutputSampleRateHz(); - encoding = audioProcessor.getOutputEncoding(); + inputAudioFormat = outputAudioFormat; } } + sampleRate = outputAudioFormat.sampleRate; + channelCount = outputAudioFormat.channelCount; + encoding = outputAudioFormat.encoding; } int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index 1e6af46fbc..a75e675e6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; @@ -29,27 +30,21 @@ import java.nio.ByteBuffer; private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF; @Override - public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (!Util.isEncodingHighResolutionIntegerPcm(encoding)) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (!Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - setInputFormat(sampleRateHz, channelCount, encoding); - } - - @Override - public boolean isActive() { - return Util.isEncodingHighResolutionIntegerPcm(encoding); - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_FLOAT; + return Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding) + ? new AudioFormat( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT) + : AudioFormat.NOT_SET; } @Override public void queueInput(ByteBuffer inputBuffer) { - boolean isInput32Bit = encoding == C.ENCODING_PCM_32BIT; + Assertions.checkState(Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)); + boolean isInput32Bit = inputAudioFormat.encoding == C.ENCODING_PCM_32BIT; int position = inputBuffer.position(); int limit = inputBuffer.limit(); int size = limit - position; @@ -65,7 +60,7 @@ import java.nio.ByteBuffer; | ((inputBuffer.get(i + 3) & 0xFF) << 24); writePcm32BitFloat(pcm32BitInteger, buffer); } - } else { + } else { // Input is 24-bit PCM. for (int i = position; i < limit; i += 3) { int pcm32BitInteger = ((inputBuffer.get(i) & 0xFF) << 8) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 4817504933..1bfa1897c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -26,23 +26,17 @@ import java.nio.ByteBuffer; /* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @Override - public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + throw new UnhandledAudioFormatException(inputAudioFormat); } - setInputFormat(sampleRateHz, channelCount, encoding); - } - - @Override - public boolean isActive() { - return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; + return encoding != C.ENCODING_PCM_16BIT + ? new AudioFormat( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT) + : AudioFormat.NOT_SET; } @Override @@ -52,7 +46,7 @@ import java.nio.ByteBuffer; int limit = inputBuffer.limit(); int size = limit - position; int resampledSize; - switch (encoding) { + switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; @@ -74,7 +68,7 @@ import java.nio.ByteBuffer; // Resample the little endian input and update the input/output buffers. ByteBuffer buffer = replaceOutputBuffer(resampledSize); - switch (encoding) { + switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index 35e8f8aa5f..59feed9bd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -119,18 +119,17 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { // AudioProcessor implementation. @Override - public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - bytesPerFrame = channelCount * 2; - setInputFormat(sampleRateHz, channelCount, encoding); + return enabled ? inputAudioFormat : AudioFormat.NOT_SET; } @Override public boolean isActive() { - return super.isActive() && enabled; + return enabled; } @Override @@ -165,7 +164,8 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { @Override protected void onFlush() { - if (isActive()) { + if (enabled) { + bytesPerFrame = inputAudioFormat.bytesPerFrame; int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame; if (maybeSilenceBuffer.length != maybeSilenceBufferSize) { maybeSilenceBuffer = new byte[maybeSilenceBufferSize]; @@ -317,7 +317,7 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio. */ private int durationUsToFrames(long durationUs) { - return (int) ((durationUs * sampleRateHz) / C.MICROS_PER_SECOND); + return (int) ((durationUs * inputAudioFormat.sampleRate) / C.MICROS_PER_SECOND); } /** 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 5bffc8fc68..4ebadab80c 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.C.Encoding; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -62,12 +61,12 @@ public final class SonicAudioProcessor implements AudioProcessor { */ private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024; - private int channelCount; - private int sampleRateHz; + private int pendingOutputSampleRate; private float speed; private float pitch; - private int outputSampleRateHz; - private int pendingOutputSampleRateHz; + + private AudioFormat inputAudioFormat; + private AudioFormat outputAudioFormat; private boolean pendingSonicRecreation; @Nullable private Sonic sonic; @@ -84,13 +83,12 @@ public final class SonicAudioProcessor implements AudioProcessor { public SonicAudioProcessor() { speed = 1f; pitch = 1f; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - outputSampleRateHz = Format.NO_VALUE; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; - pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; + pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE; } /** @@ -129,14 +127,14 @@ public final class SonicAudioProcessor implements AudioProcessor { /** * Sets the sample rate for output audio, in hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output - * audio at the same sample rate as the input. After calling this method, call - * {@link #configure(int, int, int)} to start using the new sample rate. + * audio at the same sample rate as the input. After calling this method, call {@link + * #configure(AudioFormat)} to start using the new sample rate. * * @param sampleRateHz The sample rate for output audio, in hertz. - * @see #configure(int, int, int) + * @see #configure(AudioFormat) */ public void setOutputSampleRateHz(int sampleRateHz) { - pendingOutputSampleRateHz = sampleRateHz; + pendingOutputSampleRate = sampleRateHz; } /** @@ -149,50 +147,39 @@ public final class SonicAudioProcessor implements AudioProcessor { */ public long scaleDurationForSpeedup(long duration) { if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) { - return outputSampleRateHz == sampleRateHz + return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate ? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes) - : Util.scaleLargeTimestamp(duration, inputBytes * outputSampleRateHz, - outputBytes * sampleRateHz); + : Util.scaleLargeTimestamp( + duration, + inputBytes * outputAudioFormat.sampleRate, + outputBytes * inputAudioFormat.sampleRate); } else { return (long) ((double) speed * duration); } } @Override - public void configure(int sampleRateHz, int channelCount, @Encoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - int outputSampleRateHz = pendingOutputSampleRateHz == SAMPLE_RATE_NO_CHANGE - ? sampleRateHz : pendingOutputSampleRateHz; - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - this.outputSampleRateHz = outputSampleRateHz; + int outputSampleRateHz = + pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE + ? inputAudioFormat.sampleRate + : pendingOutputSampleRate; + this.inputAudioFormat = inputAudioFormat; + this.outputAudioFormat = + new AudioFormat(outputSampleRateHz, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT); pendingSonicRecreation = true; + return outputAudioFormat; } @Override public boolean isActive() { - return sampleRateHz != Format.NO_VALUE + return outputAudioFormat.sampleRate != Format.NO_VALUE && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD - || outputSampleRateHz != sampleRateHz); - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; - } - - @Override - public int getOutputSampleRateHz() { - return outputSampleRateHz; + || outputAudioFormat.sampleRate != inputAudioFormat.sampleRate); } @Override @@ -245,7 +232,13 @@ public final class SonicAudioProcessor implements AudioProcessor { public void flush() { if (isActive()) { if (pendingSonicRecreation) { - sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz); + sonic = + new Sonic( + inputAudioFormat.sampleRate, + inputAudioFormat.channelCount, + speed, + pitch, + outputAudioFormat.sampleRate); } else if (sonic != null) { sonic.flush(); } @@ -260,13 +253,12 @@ public final class SonicAudioProcessor implements AudioProcessor { public void reset() { speed = 1f; pitch = 1f; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - outputSampleRateHz = Format.NO_VALUE; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; - pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; + pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE; pendingSonicRecreation = false; sonic = null; inputBytes = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java index 49b17916dc..652e3eea54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -64,8 +64,9 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } @Override - public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - setInputFormat(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) { + // This processor is always active (if passed to the sink) and outputs its input. + return inputAudioFormat; } @Override @@ -81,7 +82,8 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { @Override protected void onFlush() { if (isActive()) { - audioBufferSink.flush(sampleRateHz, channelCount, encoding); + audioBufferSink.flush( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index 161b7eb652..f7d7529876 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -24,7 +24,6 @@ import java.nio.ByteBuffer; @C.PcmEncoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT; - private boolean isActive; private int trimStartFrames; private int trimEndFrames; private int bytesPerFrame; @@ -42,7 +41,7 @@ import java.nio.ByteBuffer; /** * Sets the number of audio frames to trim from the start and end of audio passed to this - * processor. After calling this method, call {@link #configure(int, int, int)} to apply the new + * processor. After calling this method, call {@link #configure(AudioFormat)} to apply the new * trimming frame counts. * * @param trimStartFrames The number of audio frames to trim from the start of audio. @@ -68,26 +67,20 @@ import java.nio.ByteBuffer; } @Override - public void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (encoding != OUTPUT_ENCODING) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != OUTPUT_ENCODING) { + throw new UnhandledAudioFormatException(inputAudioFormat); } if (endBufferSize > 0) { trimmedFrameCount += endBufferSize / bytesPerFrame; } - bytesPerFrame = Util.getPcmFrameSize(OUTPUT_ENCODING, channelCount); + bytesPerFrame = inputAudioFormat.bytesPerFrame; endBuffer = new byte[trimEndFrames * bytesPerFrame]; endBufferSize = 0; pendingTrimStartBytes = trimStartFrames * bytesPerFrame; - isActive = trimStartFrames != 0 || trimEndFrames != 0; receivedInputSinceConfigure = false; - setInputFormat(sampleRateHz, channelCount, encoding); - } - - @Override - public boolean isActive() { - return isActive; + return trimStartFrames != 0 || trimEndFrames != 0 ? inputAudioFormat : AudioFormat.NOT_SET; } @Override @@ -140,7 +133,6 @@ import java.nio.ByteBuffer; buffer.flip(); } - @SuppressWarnings("ReferenceEquality") @Override public ByteBuffer getOutput() { if (super.isEnded() && endBufferSize > 0) { @@ -155,7 +147,6 @@ import java.nio.ByteBuffer; return super.getOutput(); } - @SuppressWarnings("ReferenceEquality") @Override public boolean isEnded() { return super.isEnded() && endBufferSize == 0; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java index 854975e654..e8eb530d99 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -31,8 +32,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class SilenceSkippingAudioProcessorTest { - private static final int TEST_SIGNAL_SAMPLE_RATE_HZ = 1000; - private static final int TEST_SIGNAL_CHANNEL_COUNT = 2; + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat( + /* sampleRate= */ 1000, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); private static final int TEST_SIGNAL_SILENCE_DURATION_MS = 1000; private static final int TEST_SIGNAL_NOISE_DURATION_MS = 1000; private static final int TEST_SIGNAL_FRAME_COUNT = 100000; @@ -52,8 +54,7 @@ public final class SilenceSkippingAudioProcessorTest { silenceSkippingAudioProcessor.setEnabled(true); // When configuring it. - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); // It's active. assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); @@ -65,8 +66,7 @@ public final class SilenceSkippingAudioProcessorTest { silenceSkippingAudioProcessor.setEnabled(false); // When configuring it. - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); // It's not active. assertThat(silenceSkippingAudioProcessor.isActive()).isFalse(); @@ -76,8 +76,7 @@ public final class SilenceSkippingAudioProcessorTest { public void testDefaultProcessor_isNotEnabled() throws Exception { // Given a processor in its default state. // When reconfigured. - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); // It's not active. assertThat(silenceSkippingAudioProcessor.isActive()).isFalse(); @@ -88,16 +87,13 @@ public final class SilenceSkippingAudioProcessorTest { // Given a signal with only noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( - TEST_SIGNAL_SAMPLE_RATE_HZ, - TEST_SIGNAL_CHANNEL_COUNT, TEST_SIGNAL_SILENCE_DURATION_MS, /* noiseDurationMs= */ 0, TEST_SIGNAL_FRAME_COUNT); // When processing the entire signal. silenceSkippingAudioProcessor.setEnabled(true); - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); silenceSkippingAudioProcessor.flush(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = @@ -113,8 +109,6 @@ public final class SilenceSkippingAudioProcessorTest { // Given a signal with only silence. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( - TEST_SIGNAL_SAMPLE_RATE_HZ, - TEST_SIGNAL_CHANNEL_COUNT, /* silenceDurationMs= */ 0, TEST_SIGNAL_NOISE_DURATION_MS, TEST_SIGNAL_FRAME_COUNT); @@ -123,8 +117,7 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); silenceSkippingAudioProcessor.flush(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = @@ -141,8 +134,6 @@ public final class SilenceSkippingAudioProcessorTest { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( - TEST_SIGNAL_SAMPLE_RATE_HZ, - TEST_SIGNAL_CHANNEL_COUNT, TEST_SIGNAL_SILENCE_DURATION_MS, TEST_SIGNAL_NOISE_DURATION_MS, TEST_SIGNAL_FRAME_COUNT); @@ -151,8 +142,7 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); silenceSkippingAudioProcessor.flush(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = @@ -169,8 +159,6 @@ public final class SilenceSkippingAudioProcessorTest { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( - TEST_SIGNAL_SAMPLE_RATE_HZ, - TEST_SIGNAL_CHANNEL_COUNT, TEST_SIGNAL_SILENCE_DURATION_MS, TEST_SIGNAL_NOISE_DURATION_MS, TEST_SIGNAL_FRAME_COUNT); @@ -179,8 +167,7 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); silenceSkippingAudioProcessor.flush(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = @@ -197,8 +184,6 @@ public final class SilenceSkippingAudioProcessorTest { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( - TEST_SIGNAL_SAMPLE_RATE_HZ, - TEST_SIGNAL_CHANNEL_COUNT, TEST_SIGNAL_SILENCE_DURATION_MS, TEST_SIGNAL_NOISE_DURATION_MS, TEST_SIGNAL_FRAME_COUNT); @@ -207,8 +192,7 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); silenceSkippingAudioProcessor.flush(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); long totalOutputFrames = @@ -224,8 +208,6 @@ public final class SilenceSkippingAudioProcessorTest { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( - TEST_SIGNAL_SAMPLE_RATE_HZ, - TEST_SIGNAL_CHANNEL_COUNT, TEST_SIGNAL_SILENCE_DURATION_MS, TEST_SIGNAL_NOISE_DURATION_MS, TEST_SIGNAL_FRAME_COUNT); @@ -234,8 +216,7 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); silenceSkippingAudioProcessor.setEnabled(true); - silenceSkippingAudioProcessor.configure( - TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); silenceSkippingAudioProcessor.flush(); assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE); @@ -253,6 +234,7 @@ public final class SilenceSkippingAudioProcessorTest { SilenceSkippingAudioProcessor processor, InputBufferProvider inputBufferProvider, int inputBufferSize) { + int bytesPerFrame = AUDIO_FORMAT.bytesPerFrame; processor.flush(); long totalOutputFrames = 0; while (inputBufferProvider.hasRemaining()) { @@ -260,14 +242,14 @@ public final class SilenceSkippingAudioProcessorTest { while (inputBuffer.hasRemaining()) { processor.queueInput(inputBuffer); ByteBuffer outputBuffer = processor.getOutput(); - totalOutputFrames += outputBuffer.remaining() / (2 * processor.getOutputChannelCount()); + totalOutputFrames += outputBuffer.remaining() / bytesPerFrame; outputBuffer.clear(); } } processor.queueEndOfStream(); while (!processor.isEnded()) { ByteBuffer outputBuffer = processor.getOutput(); - totalOutputFrames += outputBuffer.remaining() / (2 * processor.getOutputChannelCount()); + totalOutputFrames += outputBuffer.remaining() / bytesPerFrame; outputBuffer.clear(); } return totalOutputFrames; @@ -278,16 +260,16 @@ public final class SilenceSkippingAudioProcessorTest { * between silence/noise of the specified durations to fill {@code totalFrameCount}. */ private static InputBufferProvider getInputBufferProviderForAlternatingSilenceAndNoise( - int sampleRateHz, - int channelCount, int silenceDurationMs, int noiseDurationMs, int totalFrameCount) { + int sampleRate = AUDIO_FORMAT.sampleRate; + int channelCount = AUDIO_FORMAT.channelCount; Pcm16BitAudioBuilder audioBuilder = new Pcm16BitAudioBuilder(channelCount, totalFrameCount); while (!audioBuilder.isFull()) { - int silenceDurationFrames = (silenceDurationMs * sampleRateHz) / 1000; + int silenceDurationFrames = (silenceDurationMs * sampleRate) / 1000; audioBuilder.appendFrames(/* count= */ silenceDurationFrames, /* channelLevels= */ (short) 0); - int noiseDurationFrames = (noiseDurationMs * sampleRateHz) / 1000; + int noiseDurationFrames = (noiseDurationMs * sampleRate) / 1000; audioBuilder.appendFrames( /* count= */ noiseDurationFrames, /* channelLevels= */ Short.MAX_VALUE); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java index 837d7a97a4..e6b448774c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,6 +30,16 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class SonicAudioProcessorTest { + private static final AudioFormat AUDIO_FORMAT_22050_HZ = + new AudioFormat( + /* sampleRate= */ 22050, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); + private static final AudioFormat AUDIO_FORMAT_44100_HZ = + new AudioFormat( + /* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); + private static final AudioFormat AUDIO_FORMAT_48000_HZ = + new AudioFormat( + /* sampleRate= */ 48000, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); + private SonicAudioProcessor sonicAudioProcessor; @Before @@ -39,59 +51,36 @@ public final class SonicAudioProcessorTest { public void testReconfigureWithSameSampleRate() throws Exception { // When configured for resampling from 44.1 kHz to 48 kHz, the output sample rate is correct. sonicAudioProcessor.setOutputSampleRateHz(48000); - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + AudioFormat outputAudioFormat = sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); assertThat(sonicAudioProcessor.isActive()).isTrue(); + assertThat(outputAudioFormat.sampleRate).isEqualTo(48000); // When reconfigured with 48 kHz input, there is no resampling. - sonicAudioProcessor.configure(48000, 2, C.ENCODING_PCM_16BIT); - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + outputAudioFormat = sonicAudioProcessor.configure(AUDIO_FORMAT_48000_HZ); assertThat(sonicAudioProcessor.isActive()).isFalse(); + assertThat(outputAudioFormat.sampleRate).isEqualTo(48000); // When reconfigure with 44.1 kHz input, resampling is enabled again. - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + outputAudioFormat = sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); assertThat(sonicAudioProcessor.isActive()).isTrue(); + assertThat(outputAudioFormat.sampleRate).isEqualTo(48000); } @Test public void testNoSampleRateChange() throws Exception { // Configure for resampling 44.1 kHz to 48 kHz. sonicAudioProcessor.setOutputSampleRateHz(48000); - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); + assertThat(sonicAudioProcessor.isActive()).isTrue(); // Reconfigure to not modify the sample rate. sonicAudioProcessor.setOutputSampleRateHz(SonicAudioProcessor.SAMPLE_RATE_NO_CHANGE); - sonicAudioProcessor.configure(22050, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.configure(AUDIO_FORMAT_22050_HZ); // The sample rate is unmodified, and the audio processor is not active. - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(22050); assertThat(sonicAudioProcessor.isActive()).isFalse(); } - @Test - public void testBecomesActiveAfterConfigure() throws Exception { - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); - // Set a new sample rate. - sonicAudioProcessor.setOutputSampleRateHz(22050); - // The new sample rate is not active yet. - assertThat(sonicAudioProcessor.isActive()).isFalse(); - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(44100); - } - - @Test - public void testSampleRateChangeBecomesActiveAfterConfigure() throws Exception { - // Configure for resampling 44.1 kHz to 48 kHz. - sonicAudioProcessor.setOutputSampleRateHz(48000); - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); - // Set a new sample rate, which isn't active yet. - sonicAudioProcessor.setOutputSampleRateHz(22050); - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); - // The new sample rate takes effect on reconfiguration. - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); - assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(22050); - } - @Test public void testIsActiveWithSpeedChange() throws Exception { sonicAudioProcessor.setSpeed(1.5f); - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); sonicAudioProcessor.flush(); assertThat(sonicAudioProcessor.isActive()).isTrue(); } @@ -99,35 +88,45 @@ public final class SonicAudioProcessorTest { @Test public void testIsActiveWithPitchChange() throws Exception { sonicAudioProcessor.setPitch(1.5f); - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); sonicAudioProcessor.flush(); assertThat(sonicAudioProcessor.isActive()).isTrue(); } @Test public void testIsNotActiveWithNoChange() throws Exception { - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ); assertThat(sonicAudioProcessor.isActive()).isFalse(); } @Test public void testDoesNotSupportNon16BitInput() throws Exception { try { - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_8BIT); + sonicAudioProcessor.configure( + new AudioFormat( + /* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_8BIT)); fail(); - } catch (AudioProcessor.UnhandledFormatException e) { + } catch (UnhandledAudioFormatException e) { // Expected. } try { - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_24BIT); + sonicAudioProcessor.configure( + new AudioFormat( + /* sampleRate= */ 44100, + /* channelCount= */ 2, + /* encoding= */ C.ENCODING_PCM_24BIT)); fail(); - } catch (AudioProcessor.UnhandledFormatException e) { + } catch (UnhandledAudioFormatException e) { // Expected. } try { - sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_32BIT); + sonicAudioProcessor.configure( + new AudioFormat( + /* sampleRate= */ 44100, + /* channelCount= */ 2, + /* encoding= */ C.ENCODING_PCM_32BIT)); fail(); - } catch (AudioProcessor.UnhandledFormatException e) { + } catch (UnhandledAudioFormatException e) { // Expected. } } From b9f79b40f872e6cf6ca502e44236aee57fa63277 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 26 Nov 2019 09:09:40 +0000 Subject: [PATCH 767/807] Remove redundant flush() calls from AudioProcessors flush() is guaranteed to be called in all these cases anyway. Also clarify documentation for AudioProcessor-specific methods that can change the 'active' flag. Issue: #6601 PiperOrigin-RevId: 282515255 --- .../audio/SilenceSkippingAudioProcessor.java | 6 +++--- .../exoplayer2/audio/SonicAudioProcessor.java | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index 59feed9bd2..2a98d2fb25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -98,14 +98,14 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { } /** - * Sets whether to skip silence in the input. Calling this method will discard any data buffered - * within the processor, and may update the value returned by {@link #isActive()}. + * Sets whether to skip silence in the input. This method may only be called after draining data + * through the processor. The value returned by {@link #isActive()} may change, and the processor + * must be {@link #flush() flushed} before queueing more data. * * @param enabled Whether to skip silence in the input. */ public void setEnabled(boolean enabled) { this.enabled = enabled; - flush(); } /** 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 4ebadab80c..c683f60e76 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 @@ -92,8 +92,9 @@ public final class SonicAudioProcessor implements AudioProcessor { } /** - * Sets the playback speed. Calling this method will discard any data buffered within the - * processor, and may update the value returned by {@link #isActive()}. + * Sets the playback speed. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. * * @param speed The requested new playback speed. * @return The actual new playback speed. @@ -104,13 +105,13 @@ public final class SonicAudioProcessor implements AudioProcessor { this.speed = speed; pendingSonicRecreation = true; } - flush(); return speed; } /** - * Sets the playback pitch. Calling this method will discard any data buffered within the - * processor, and may update the value returned by {@link #isActive()}. + * Sets the playback pitch. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. * * @param pitch The requested new pitch. * @return The actual new pitch. @@ -121,16 +122,15 @@ public final class SonicAudioProcessor implements AudioProcessor { this.pitch = pitch; pendingSonicRecreation = true; } - flush(); return pitch; } /** - * Sets the sample rate for output audio, in hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output + * Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output * audio at the same sample rate as the input. After calling this method, call {@link - * #configure(AudioFormat)} to start using the new sample rate. + * #configure(AudioFormat)} to configure the processor with the new sample rate. * - * @param sampleRateHz The sample rate for output audio, in hertz. + * @param sampleRateHz The sample rate for output audio, in Hertz. * @see #configure(AudioFormat) */ public void setOutputSampleRateHz(int sampleRateHz) { From d30b0285a3136165d12ff8fd89bb585fac5f385f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 26 Nov 2019 09:10:39 +0000 Subject: [PATCH 768/807] Fix audio processor draining for reconfiguration When transitioning to a new stream in a different format, the audio processors are reconfigured. After this, they are drained and then flushed so that they are ready to handle data in updated formats for the new stream. Before this change, some audio processors made the assumption that after reconfiguration no more input would be queued in their old input format, but this assumption is not correct: during draining more input may be queued. Fix this behavior so that the new configuration is not referred to while draining and only becomes active once flushed. Issue: #6601 PiperOrigin-RevId: 282515359 --- RELEASENOTES.md | 4 ++ .../exoplayer2/ext/gvr/GvrAudioProcessor.java | 12 +++--- .../exoplayer2/audio/AudioProcessor.java | 5 ++- .../exoplayer2/audio/BaseAudioProcessor.java | 21 +++++++--- .../audio/ChannelMappingAudioProcessor.java | 17 +++++---- .../exoplayer2/audio/SonicAudioProcessor.java | 18 ++++++--- .../exoplayer2/audio/TeeAudioProcessor.java | 11 +++++- .../audio/TrimmingAudioProcessor.java | 38 ++++++++++--------- 8 files changed, 81 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c175e7f240..388b6c31e2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -79,6 +79,8 @@ item in a playlist of Opus streams. * Workaround broken raw audio decoding on Oppo R9 ([#5782](https://github.com/google/ExoPlayer/issues/5782)). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. @@ -138,6 +140,8 @@ [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* Downloads: Merge downloads in `SegmentDownloader` to improve overall download + speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). ### 2.10.8 (2019-11-19) ### diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index 7748e053b4..8ba33290ea 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -43,7 +43,7 @@ public final class GvrAudioProcessor implements AudioProcessor { private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID; - private AudioFormat inputAudioFormat; + private AudioFormat pendingInputAudioFormat; private int pendingGvrAudioSurroundFormat; @Nullable private GvrAudioSurround gvrAudioSurround; private ByteBuffer buffer; @@ -58,7 +58,7 @@ public final class GvrAudioProcessor implements AudioProcessor { public GvrAudioProcessor() { // Use the identity for the initial orientation. w = 1f; - inputAudioFormat = AudioFormat.NOT_SET; + pendingInputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; } @@ -116,7 +116,7 @@ public final class GvrAudioProcessor implements AudioProcessor { buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) .order(ByteOrder.nativeOrder()); } - this.inputAudioFormat = inputAudioFormat; + pendingInputAudioFormat = inputAudioFormat; return new AudioFormat(inputAudioFormat.sampleRate, OUTPUT_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); } @@ -164,8 +164,8 @@ public final class GvrAudioProcessor implements AudioProcessor { gvrAudioSurround = new GvrAudioSurround( pendingGvrAudioSurroundFormat, - inputAudioFormat.sampleRate, - inputAudioFormat.channelCount, + pendingInputAudioFormat.sampleRate, + pendingInputAudioFormat.channelCount, FRAMES_PER_OUTPUT_BUFFER); gvrAudioSurround.updateNativeOrientation(w, x, y, z); pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; @@ -180,7 +180,7 @@ public final class GvrAudioProcessor implements AudioProcessor { maybeReleaseGvrAudioSurround(); updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f); inputEnded = false; - inputAudioFormat = AudioFormat.NOT_SET; + pendingInputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index 1b0ddff16c..f75b2cd317 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -88,8 +88,9 @@ public interface AudioProcessor { * the configured output audio format if this instance is active. * *

        After calling this method, it is necessary to {@link #flush()} the processor to apply the - * new configuration before queueing more data. You can (optionally) first drain output in the - * previous configuration by calling {@link #queueEndOfStream()} and {@link #getOutput()}. + * new configuration. Before applying the new configuration, it is safe to queue input and get + * output in the old input/output formats. Call {@link #queueEndOfStream()} when no more input + * will be supplied in the old input format. * * @param inputAudioFormat The format of audio that will be queued after the next call to {@link * #flush()}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java index c9e5465644..41cb436504 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java @@ -26,10 +26,13 @@ import java.nio.ByteOrder; */ public abstract class BaseAudioProcessor implements AudioProcessor { - /** The configured input audio format. */ + /** The current input audio format. */ protected AudioFormat inputAudioFormat; + /** The current output audio format. */ + protected AudioFormat outputAudioFormat; - private AudioFormat outputAudioFormat; + private AudioFormat pendingInputAudioFormat; + private AudioFormat pendingOutputAudioFormat; private ByteBuffer buffer; private ByteBuffer outputBuffer; private boolean inputEnded; @@ -37,6 +40,8 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public BaseAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; outputAudioFormat = AudioFormat.NOT_SET; } @@ -44,14 +49,14 @@ public abstract class BaseAudioProcessor implements AudioProcessor { @Override public final AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { - this.inputAudioFormat = inputAudioFormat; - outputAudioFormat = onConfigure(inputAudioFormat); - return isActive() ? outputAudioFormat : AudioFormat.NOT_SET; + pendingInputAudioFormat = inputAudioFormat; + pendingOutputAudioFormat = onConfigure(inputAudioFormat); + return isActive() ? pendingOutputAudioFormat : AudioFormat.NOT_SET; } @Override public boolean isActive() { - return outputAudioFormat != AudioFormat.NOT_SET; + return pendingOutputAudioFormat != AudioFormat.NOT_SET; } @Override @@ -79,6 +84,8 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public final void flush() { outputBuffer = EMPTY_BUFFER; inputEnded = false; + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; onFlush(); } @@ -86,6 +93,8 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public final void reset() { flush(); buffer = EMPTY_BUFFER; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; outputAudioFormat = AudioFormat.NOT_SET; onReset(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 87be1ee88a..4fb6af1af4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -24,12 +24,10 @@ import java.nio.ByteBuffer; * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ -// the constructor does not initialize fields: pendingOutputChannels, outputChannels @SuppressWarnings("nullness:initialization.fields.uninitialized") /* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { @Nullable private int[] pendingOutputChannels; - @Nullable private int[] outputChannels; /** @@ -47,9 +45,7 @@ import java.nio.ByteBuffer; @Override public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { - outputChannels = pendingOutputChannels; - - int[] outputChannels = this.outputChannels; + @Nullable int[] outputChannels = pendingOutputChannels; if (outputChannels == null) { return AudioFormat.NOT_SET; } @@ -76,19 +72,24 @@ import java.nio.ByteBuffer; int[] outputChannels = Assertions.checkNotNull(this.outputChannels); int position = inputBuffer.position(); int limit = inputBuffer.limit(); - int frameCount = (limit - position) / (2 * inputAudioFormat.channelCount); - int outputSize = frameCount * outputChannels.length * 2; + int frameCount = (limit - position) / inputAudioFormat.bytesPerFrame; + int outputSize = frameCount * outputAudioFormat.bytesPerFrame; ByteBuffer buffer = replaceOutputBuffer(outputSize); while (position < limit) { for (int channelIndex : outputChannels) { buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); } - position += inputAudioFormat.channelCount * 2; + position += inputAudioFormat.bytesPerFrame; } inputBuffer.position(limit); buffer.flip(); } + @Override + protected void onFlush() { + outputChannels = pendingOutputChannels; + } + @Override protected void onReset() { outputChannels = null; 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 c683f60e76..b9a59cd620 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 @@ -65,6 +65,8 @@ public final class SonicAudioProcessor implements AudioProcessor { private float speed; private float pitch; + private AudioFormat pendingInputAudioFormat; + private AudioFormat pendingOutputAudioFormat; private AudioFormat inputAudioFormat; private AudioFormat outputAudioFormat; @@ -83,6 +85,8 @@ public final class SonicAudioProcessor implements AudioProcessor { public SonicAudioProcessor() { speed = 1f; pitch = 1f; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; @@ -167,19 +171,19 @@ public final class SonicAudioProcessor implements AudioProcessor { pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE ? inputAudioFormat.sampleRate : pendingOutputSampleRate; - this.inputAudioFormat = inputAudioFormat; - this.outputAudioFormat = + pendingInputAudioFormat = inputAudioFormat; + pendingOutputAudioFormat = new AudioFormat(outputSampleRateHz, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT); pendingSonicRecreation = true; - return outputAudioFormat; + return pendingOutputAudioFormat; } @Override public boolean isActive() { - return outputAudioFormat.sampleRate != Format.NO_VALUE + return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD - || outputAudioFormat.sampleRate != inputAudioFormat.sampleRate); + || pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate); } @Override @@ -231,6 +235,8 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void flush() { if (isActive()) { + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; if (pendingSonicRecreation) { sonic = new Sonic( @@ -253,6 +259,8 @@ public final class SonicAudioProcessor implements AudioProcessor { public void reset() { speed = 1f; pitch = 1f; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; inputAudioFormat = AudioFormat.NOT_SET; outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java index 652e3eea54..8f39dd1d85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -80,7 +80,16 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } @Override - protected void onFlush() { + protected void onQueueEndOfStream() { + flushSinkIfActive(); + } + + @Override + protected void onReset() { + flushSinkIfActive(); + } + + private void flushSinkIfActive() { if (isActive()) { audioBufferSink.flush( inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index f7d7529876..9437e4ac26 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -26,8 +26,7 @@ import java.nio.ByteBuffer; private int trimStartFrames; private int trimEndFrames; - private int bytesPerFrame; - private boolean receivedInputSinceConfigure; + private boolean reconfigurationPending; private int pendingTrimStartBytes; private byte[] endBuffer; @@ -72,14 +71,7 @@ import java.nio.ByteBuffer; if (inputAudioFormat.encoding != OUTPUT_ENCODING) { throw new UnhandledAudioFormatException(inputAudioFormat); } - if (endBufferSize > 0) { - trimmedFrameCount += endBufferSize / bytesPerFrame; - } - bytesPerFrame = inputAudioFormat.bytesPerFrame; - endBuffer = new byte[trimEndFrames * bytesPerFrame]; - endBufferSize = 0; - pendingTrimStartBytes = trimStartFrames * bytesPerFrame; - receivedInputSinceConfigure = false; + reconfigurationPending = true; return trimStartFrames != 0 || trimEndFrames != 0 ? inputAudioFormat : AudioFormat.NOT_SET; } @@ -92,11 +84,10 @@ import java.nio.ByteBuffer; if (remaining == 0) { return; } - receivedInputSinceConfigure = true; // Trim any pending start bytes from the input buffer. int trimBytes = Math.min(remaining, pendingTrimStartBytes); - trimmedFrameCount += trimBytes / bytesPerFrame; + trimmedFrameCount += trimBytes / inputAudioFormat.bytesPerFrame; pendingTrimStartBytes -= trimBytes; inputBuffer.position(position + trimBytes); if (pendingTrimStartBytes > 0) { @@ -137,10 +128,8 @@ import java.nio.ByteBuffer; public ByteBuffer getOutput() { if (super.isEnded() && endBufferSize > 0) { // Because audio processors may be drained in the middle of the stream we assume that the - // contents of the end buffer need to be output. For gapless transitions, configure will be - // always be called, which clears the end buffer as needed. When audio is actually ending we - // play the padding data which is incorrect. This behavior can be fixed once we have the - // timestamps associated with input buffers. + // contents of the end buffer need to be output. For gapless transitions, configure will + // always be called, so the end buffer is cleared in onQueueEndOfStream. replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip(); endBufferSize = 0; } @@ -152,9 +141,24 @@ import java.nio.ByteBuffer; return super.isEnded() && endBufferSize == 0; } + @Override + protected void onQueueEndOfStream() { + if (reconfigurationPending) { + // Trim audio in the end buffer. + if (endBufferSize > 0) { + trimmedFrameCount += endBufferSize / inputAudioFormat.bytesPerFrame; + } + endBufferSize = 0; + } + } + @Override protected void onFlush() { - if (receivedInputSinceConfigure) { + if (reconfigurationPending) { + reconfigurationPending = false; + endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; + pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; + } else { // Audio processors are flushed after initial configuration, so we leave the pending trim // start byte count unmodified if the processor was just configured. Otherwise we (possibly // incorrectly) assume that this is a seek to a non-zero position. We should instead check the From 9d9a2a681819dc185315cc0a0ec28e1f4207d131 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 27 Nov 2019 22:41:01 +0000 Subject: [PATCH 769/807] Remove stray release note --- RELEASENOTES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 388b6c31e2..4e8d986f44 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -140,8 +140,6 @@ [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). -* Downloads: Merge downloads in `SegmentDownloader` to improve overall download - speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). ### 2.10.8 (2019-11-19) ### From 5f465f770b41db4c71417630bcd78007a09aaddd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 29 Nov 2019 09:30:52 +0000 Subject: [PATCH 770/807] Remove AdsManager listeners on release Issue: #6687 PiperOrigin-RevId: 283023548 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4e8d986f44..884cd54a2a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -140,6 +140,9 @@ [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). +* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an + `AdEventListener` provided by the app + ([#6687](https://github.com/google/ExoPlayer/issues/6687)). ### 2.10.8 (2019-11-19) ### 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 cf0ea79da6..98dbef7c6c 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 @@ -646,6 +646,11 @@ public final class ImaAdsLoader public void release() { pendingAdRequestContext = null; if (adsManager != null) { + adsManager.removeAdErrorListener(this); + adsManager.removeAdEventListener(this); + if (adEventListener != null) { + adsManager.removeAdEventListener(adEventListener); + } adsManager.destroy(); adsManager = null; } From 815ec8afab9e86ecdc742bcb3c1bbc6012b49dbd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 09:45:10 +0000 Subject: [PATCH 771/807] Provide instructions for building extensions using Windows PowerShell PiperOrigin-RevId: 283296427 --- extensions/av1/README.md | 9 ++++++++- extensions/ffmpeg/README.md | 9 ++++++++- extensions/flac/README.md | 9 ++++++++- extensions/opus/README.md | 9 ++++++++- extensions/vp9/README.md | 9 ++++++++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/extensions/av1/README.md b/extensions/av1/README.md index b57f0b484c..276daae4e2 100644 --- a/extensions/av1/README.md +++ b/extensions/av1/README.md @@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE -## Build instructions ## +## Build instructions (Linux, macOS) ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -61,6 +61,13 @@ to configure and build libgav1 and the extension's [JNI wrapper library][]. [Ninja]: https://ninja-build.org [JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc +## Build instructions (Windows) ## + +We do not provide support for building this extension on Windows, however it +should be possible to follow the Linux instructions in [Windows PowerShell][]. + +[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell + ## Using the extension ## Once you've followed the instructions above to check out, build and depend on diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index f8120ed11b..fe4aca772a 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE -## Build instructions ## +## Build instructions (Linux, macOS) ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -66,6 +66,13 @@ cd "${FFMPEG_EXT_PATH}" && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` +## Build instructions (Windows) ## + +We do not provide support for building this extension on Windows, however it +should be possible to follow the Linux instructions in [Windows PowerShell][]. + +[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell + ## Using the extension ## Once you've followed the instructions above to check out, build and depend on diff --git a/extensions/flac/README.md b/extensions/flac/README.md index e534f9b2ac..84a92f9586 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE -## Build instructions ## +## Build instructions (Linux, macOS) ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -53,6 +53,13 @@ ${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 +## Build instructions (Windows) ## + +We do not provide support for building this extension on Windows, however it +should be possible to follow the Linux instructions in [Windows PowerShell][]. + +[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell + ## Using the extension ## Once you've followed the instructions above to check out, build and depend on diff --git a/extensions/opus/README.md b/extensions/opus/README.md index ce88f0ef7d..05448f2073 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE -## Build instructions ## +## Build instructions (Linux, macOS) ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -58,6 +58,13 @@ ${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 +## Build instructions (Windows) ## + +We do not provide support for building this extension on Windows, however it +should be possible to follow the Linux instructions in [Windows PowerShell][]. + +[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell + ## Notes ## * Every time there is a change to the libopus checkout: diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 03d9b7413d..71241d9a4f 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE -## Build instructions ## +## Build instructions (Linux, macOS) ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -68,6 +68,13 @@ ${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 +## Build instructions (Windows) ## + +We do not provide support for building this extension on Windows, however it +should be possible to follow the Linux instructions in [Windows PowerShell][]. + +[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell + ## Notes ## * Every time there is a change to the libvpx checkout: From 8ae654c48584c4dd04c9f024a97b0191135d4d59 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 10:59:07 +0000 Subject: [PATCH 772/807] Add layer of indirection for drawables This allows easy overriding of the resources by app developers Issue: #6709 PiperOrigin-RevId: 283306121 --- RELEASENOTES.md | 3 +++ ...reen_enter.xml => exo_icon_fullscreen_enter.xml} | 0 ...screen_exit.xml => exo_icon_fullscreen_exit.xml} | 0 ...trols_repeat_all.xml => exo_icon_repeat_all.xml} | 0 ...trols_repeat_off.xml => exo_icon_repeat_off.xml} | 0 ...trols_repeat_one.xml => exo_icon_repeat_one.xml} | 0 ...ols_shuffle_off.xml => exo_icon_shuffle_off.xml} | 0 ...trols_shuffle_on.xml => exo_icon_shuffle_on.xml} | 0 ...on_small_icon.png => exo_icon_circular_play.png} | Bin ...reen_enter.png => exo_icon_fullscreen_enter.png} | Bin ...screen_exit.png => exo_icon_fullscreen_exit.png} | Bin ...trols_repeat_all.png => exo_icon_repeat_all.png} | Bin ...trols_repeat_off.png => exo_icon_repeat_off.png} | Bin ...trols_repeat_one.png => exo_icon_repeat_one.png} | Bin ...ols_shuffle_off.png => exo_icon_shuffle_off.png} | Bin ...trols_shuffle_on.png => exo_icon_shuffle_on.png} | Bin ...on_small_icon.png => exo_icon_circular_play.png} | Bin ...reen_enter.png => exo_icon_fullscreen_enter.png} | Bin ...screen_exit.png => exo_icon_fullscreen_exit.png} | Bin ...trols_repeat_all.png => exo_icon_repeat_all.png} | Bin ...trols_repeat_off.png => exo_icon_repeat_off.png} | Bin ...trols_repeat_one.png => exo_icon_repeat_one.png} | Bin ...ols_shuffle_off.png => exo_icon_shuffle_off.png} | Bin ...trols_shuffle_on.png => exo_icon_shuffle_on.png} | Bin ...on_small_icon.png => exo_icon_circular_play.png} | Bin ...reen_enter.png => exo_icon_fullscreen_enter.png} | Bin ...screen_exit.png => exo_icon_fullscreen_exit.png} | Bin ...trols_repeat_all.png => exo_icon_repeat_all.png} | Bin ...trols_repeat_off.png => exo_icon_repeat_off.png} | Bin ...trols_repeat_one.png => exo_icon_repeat_one.png} | Bin ...ols_shuffle_off.png => exo_icon_shuffle_off.png} | Bin ...trols_shuffle_on.png => exo_icon_shuffle_on.png} | Bin ...on_small_icon.png => exo_icon_circular_play.png} | Bin ...reen_enter.png => exo_icon_fullscreen_enter.png} | Bin ...screen_exit.png => exo_icon_fullscreen_exit.png} | Bin ...trols_repeat_all.png => exo_icon_repeat_all.png} | Bin ...trols_repeat_off.png => exo_icon_repeat_off.png} | Bin ...trols_repeat_one.png => exo_icon_repeat_one.png} | Bin ...ols_shuffle_off.png => exo_icon_shuffle_off.png} | Bin ...trols_shuffle_on.png => exo_icon_shuffle_on.png} | Bin ...on_small_icon.png => exo_icon_circular_play.png} | Bin ...reen_enter.png => exo_icon_fullscreen_enter.png} | Bin ...screen_exit.png => exo_icon_fullscreen_exit.png} | Bin ...trols_repeat_all.png => exo_icon_repeat_all.png} | Bin ...trols_repeat_off.png => exo_icon_repeat_off.png} | Bin ...trols_repeat_one.png => exo_icon_repeat_one.png} | Bin ...ols_shuffle_off.png => exo_icon_shuffle_off.png} | Bin ...trols_shuffle_on.png => exo_icon_shuffle_on.png} | Bin ...on_small_icon.png => exo_icon_circular_play.png} | Bin library/ui/src/main/res/values/drawables.xml | 9 +++++++++ library/ui/src/main/res/values/styles.xml | 2 +- 51 files changed, 13 insertions(+), 1 deletion(-) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_fullscreen_enter.xml => exo_icon_fullscreen_enter.xml} (100%) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_fullscreen_exit.xml => exo_icon_fullscreen_exit.xml} (100%) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_repeat_all.xml => exo_icon_repeat_all.xml} (100%) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_repeat_off.xml => exo_icon_repeat_off.xml} (100%) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_repeat_one.xml => exo_icon_repeat_one.xml} (100%) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_shuffle_off.xml => exo_icon_shuffle_off.xml} (100%) rename library/ui/src/main/res/drawable-anydpi-v21/{exo_controls_shuffle_on.xml => exo_icon_shuffle_on.xml} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_notification_small_icon.png => exo_icon_circular_play.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_fullscreen_enter.png => exo_icon_fullscreen_enter.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_fullscreen_exit.png => exo_icon_fullscreen_exit.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_repeat_all.png => exo_icon_repeat_all.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_repeat_off.png => exo_icon_repeat_off.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_repeat_one.png => exo_icon_repeat_one.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_shuffle_off.png => exo_icon_shuffle_off.png} (100%) rename library/ui/src/main/res/drawable-hdpi/{exo_controls_shuffle_on.png => exo_icon_shuffle_on.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_notification_small_icon.png => exo_icon_circular_play.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_fullscreen_enter.png => exo_icon_fullscreen_enter.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_fullscreen_exit.png => exo_icon_fullscreen_exit.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_repeat_all.png => exo_icon_repeat_all.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_repeat_off.png => exo_icon_repeat_off.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_repeat_one.png => exo_icon_repeat_one.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_shuffle_off.png => exo_icon_shuffle_off.png} (100%) rename library/ui/src/main/res/drawable-ldpi/{exo_controls_shuffle_on.png => exo_icon_shuffle_on.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_notification_small_icon.png => exo_icon_circular_play.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_fullscreen_enter.png => exo_icon_fullscreen_enter.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_fullscreen_exit.png => exo_icon_fullscreen_exit.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_repeat_all.png => exo_icon_repeat_all.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_repeat_off.png => exo_icon_repeat_off.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_repeat_one.png => exo_icon_repeat_one.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_shuffle_off.png => exo_icon_shuffle_off.png} (100%) rename library/ui/src/main/res/drawable-mdpi/{exo_controls_shuffle_on.png => exo_icon_shuffle_on.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_notification_small_icon.png => exo_icon_circular_play.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_fullscreen_enter.png => exo_icon_fullscreen_enter.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_fullscreen_exit.png => exo_icon_fullscreen_exit.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_repeat_all.png => exo_icon_repeat_all.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_repeat_off.png => exo_icon_repeat_off.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_repeat_one.png => exo_icon_repeat_one.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_shuffle_off.png => exo_icon_shuffle_off.png} (100%) rename library/ui/src/main/res/drawable-xhdpi/{exo_controls_shuffle_on.png => exo_icon_shuffle_on.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_notification_small_icon.png => exo_icon_circular_play.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_fullscreen_enter.png => exo_icon_fullscreen_enter.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_fullscreen_exit.png => exo_icon_fullscreen_exit.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_repeat_all.png => exo_icon_repeat_all.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_repeat_off.png => exo_icon_repeat_off.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_repeat_one.png => exo_icon_repeat_one.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_shuffle_off.png => exo_icon_shuffle_off.png} (100%) rename library/ui/src/main/res/drawable-xxhdpi/{exo_controls_shuffle_on.png => exo_icon_shuffle_on.png} (100%) rename library/ui/src/main/res/drawable-xxxhdpi/{exo_notification_small_icon.png => exo_icon_circular_play.png} (100%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 884cd54a2a..373a024eea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -85,6 +85,9 @@ * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. * Rename `spherical_view` surface type to `spherical_gl_surface_view`. + * Make it easier to override the shuffle, repeat, fullscreen, VR and small + notification icon assets + ([#6709](https://github.com/google/ExoPlayer/issues/6709)). * Analytics: * Remove `AnalyticsCollector.Factory`. Instances should be created directly, and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fullscreen_enter.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fullscreen_enter.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fullscreen_enter.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fullscreen_enter.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fullscreen_exit.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fullscreen_exit.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fullscreen_exit.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fullscreen_exit.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_repeat_all.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_repeat_all.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_repeat_off.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_repeat_off.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_repeat_one.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_repeat_one.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_shuffle_off.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_shuffle_off.xml diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_shuffle_on.xml similarity index 100% rename from library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_icon_shuffle_on.xml diff --git a/library/ui/src/main/res/drawable-hdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_circular_play.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_notification_small_icon.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_circular_play.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_fullscreen_enter.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_fullscreen_enter.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_fullscreen_enter.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_fullscreen_enter.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_fullscreen_exit.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_fullscreen_exit.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_fullscreen_exit.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_fullscreen_exit.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_repeat_all.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_repeat_all.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_repeat_off.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_off.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_repeat_off.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_repeat_one.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_one.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_repeat_one.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_shuffle_off.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_off.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_shuffle_off.png diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_on.png b/library/ui/src/main/res/drawable-hdpi/exo_icon_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle_on.png rename to library/ui/src/main/res/drawable-hdpi/exo_icon_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_circular_play.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_notification_small_icon.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_circular_play.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_enter.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_fullscreen_enter.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_enter.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_fullscreen_enter.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_exit.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_fullscreen_exit.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_fullscreen_exit.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_fullscreen_exit.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_repeat_all.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_repeat_all.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_repeat_off.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_off.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_repeat_off.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_repeat_one.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_one.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_repeat_one.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_shuffle_off.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle_off.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_shuffle_off.png diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle_on.png b/library/ui/src/main/res/drawable-ldpi/exo_icon_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle_on.png rename to library/ui/src/main/res/drawable-ldpi/exo_icon_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_circular_play.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_notification_small_icon.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_circular_play.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_fullscreen_enter.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_fullscreen_enter.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_fullscreen_enter.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_fullscreen_enter.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_fullscreen_exit.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_fullscreen_exit.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_fullscreen_exit.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_fullscreen_exit.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_repeat_all.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_repeat_all.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_repeat_off.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_repeat_off.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_repeat_one.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_repeat_one.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_shuffle_off.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_off.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_shuffle_off.png diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_on.png b/library/ui/src/main/res/drawable-mdpi/exo_icon_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle_on.png rename to library/ui/src/main/res/drawable-mdpi/exo_icon_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_circular_play.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_notification_small_icon.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_circular_play.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_fullscreen_enter.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_fullscreen_enter.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_fullscreen_enter.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_fullscreen_enter.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_fullscreen_exit.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_fullscreen_exit.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_fullscreen_exit.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_fullscreen_exit.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_repeat_all.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_repeat_all.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_repeat_off.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_off.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_repeat_off.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_repeat_one.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_one.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_repeat_one.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_shuffle_off.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_off.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_shuffle_off.png diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_on.png b/library/ui/src/main/res/drawable-xhdpi/exo_icon_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle_on.png rename to library/ui/src/main/res/drawable-xhdpi/exo_icon_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_circular_play.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_notification_small_icon.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_circular_play.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_fullscreen_enter.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_fullscreen_enter.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_fullscreen_enter.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_fullscreen_enter.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_fullscreen_exit.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_fullscreen_exit.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_fullscreen_exit.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_fullscreen_exit.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_repeat_all.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_repeat_all.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_repeat_off.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_repeat_off.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_repeat_one.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_repeat_one.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_shuffle_off.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_off.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_shuffle_off.png diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_on.png b/library/ui/src/main/res/drawable-xxhdpi/exo_icon_shuffle_on.png similarity index 100% rename from library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle_on.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_icon_shuffle_on.png diff --git a/library/ui/src/main/res/drawable-xxxhdpi/exo_notification_small_icon.png b/library/ui/src/main/res/drawable-xxxhdpi/exo_icon_circular_play.png similarity index 100% rename from library/ui/src/main/res/drawable-xxxhdpi/exo_notification_small_icon.png rename to library/ui/src/main/res/drawable-xxxhdpi/exo_icon_circular_play.png diff --git a/library/ui/src/main/res/values/drawables.xml b/library/ui/src/main/res/values/drawables.xml index 3cd9168726..84c037a8b7 100644 --- a/library/ui/src/main/res/values/drawables.xml +++ b/library/ui/src/main/res/values/drawables.xml @@ -20,6 +20,14 @@ @drawable/exo_icon_previous @drawable/exo_icon_fastforward @drawable/exo_icon_rewind + @drawable/exo_icon_repeat_all + @drawable/exo_icon_repeat_off + @drawable/exo_icon_repeat_one + @drawable/exo_icon_shuffle_off + @drawable/exo_icon_shuffle_on + @drawable/exo_icon_fullscreen_enter + @drawable/exo_icon_fullscreen_exit + @drawable/exo_icon_vr @drawable/exo_icon_play @drawable/exo_icon_pause @drawable/exo_icon_next @@ -27,4 +35,5 @@ @drawable/exo_icon_fastforward @drawable/exo_icon_rewind @drawable/exo_icon_stop + @drawable/exo_icon_circular_play diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index c458a3ea99..c271a0af6d 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -52,7 +52,7 @@ From 8e44e3b795667ad4792d3db9bfcd23d80a158e44 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 11:42:56 +0000 Subject: [PATCH 773/807] Remove LibvpxVideoRenderer from nullness blacklist PiperOrigin-RevId: 283310946 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 7fcb89dc12..4a92859b75 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 @@ -71,8 +71,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { private final boolean enableRowMultiThreadMode; private final int threads; - private VpxDecoder decoder; - private VideoFrameMetadataListener frameMetadataListener; + @Nullable private VpxDecoder decoder; + @Nullable private VideoFrameMetadataListener frameMetadataListener; /** * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer @@ -257,7 +257,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { TraceUtil.beginSection("createVpxDecoder"); int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; - decoder = + VpxDecoder decoder = new VpxDecoder( numInputBuffers, numOutputBuffers, @@ -265,6 +265,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { mediaCrypto, enableRowMultiThreadMode, threads); + this.decoder = decoder; TraceUtil.endSection(); return decoder; } From 78e72abbc47d9d2c005b49e5b17f13e415cca99c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 2 Dec 2019 13:10:01 +0000 Subject: [PATCH 774/807] Remove row VP9 multi-threading option PiperOrigin-RevId: 283319944 --- .../ext/vp9/LibvpxVideoRenderer.java | 23 ++++--------------- .../exoplayer2/ext/vp9/VpxDecoder.java | 5 ++-- 2 files changed, 7 insertions(+), 21 deletions(-) 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 4a92859b75..c84c3b41fe 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 @@ -68,7 +68,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { */ private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; - private final boolean enableRowMultiThreadMode; private final int threads; @Nullable private VpxDecoder decoder; @@ -121,8 +120,8 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, - * boolean, int, int, int)}} instead, and pass DRM-related parameters to the {@link - * MediaSource} factories. + * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource} + * factories. */ @Deprecated @SuppressWarnings("deprecation") @@ -140,7 +139,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys, - /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), /* numInputBuffers= */ 4, /* numOutputBuffers= */ 4); @@ -154,7 +152,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. @@ -165,7 +162,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, - boolean enableRowMultiThreadMode, int threads, int numInputBuffers, int numOutputBuffers) { @@ -176,7 +172,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false, - enableRowMultiThreadMode, threads, numInputBuffers, numOutputBuffers); @@ -197,13 +192,12 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { * 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 enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. * @deprecated Use {@link #LibvpxVideoRenderer(long, Handler, VideoRendererEventListener, int, - * boolean, int, int, int)}} instead, and pass DRM-related parameters to the {@link - * MediaSource} factories. + * int, int, int)}} instead, and pass DRM-related parameters to the {@link MediaSource} + * factories. */ @Deprecated public LibvpxVideoRenderer( @@ -213,7 +207,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - boolean enableRowMultiThreadMode, int threads, int numInputBuffers, int numOutputBuffers) { @@ -224,7 +217,6 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys); - this.enableRowMultiThreadMode = enableRowMultiThreadMode; this.threads = threads; this.numInputBuffers = numInputBuffers; this.numOutputBuffers = numOutputBuffers; @@ -259,12 +251,7 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; VpxDecoder decoder = new VpxDecoder( - numInputBuffers, - numOutputBuffers, - initialInputBufferSize, - mediaCrypto, - enableRowMultiThreadMode, - threads); + numInputBuffers, numOutputBuffers, initialInputBufferSize, mediaCrypto, threads); this.decoder = decoder; TraceUtil.endSection(); return decoder; 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 b4535a3e9c..98a26727ee 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 @@ -53,7 +53,6 @@ import java.nio.ByteBuffer; * @param initialInputBufferSize The initial size of each input buffer. * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. - * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled. * @param threads Number of threads libvpx will use to decode. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ @@ -62,7 +61,6 @@ import java.nio.ByteBuffer; int numOutputBuffers, int initialInputBufferSize, @Nullable ExoMediaCrypto exoMediaCrypto, - boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { super( @@ -75,7 +73,8 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(/* disableLoopFilter= */ false, enableRowMultiThreadMode, threads); + vpxDecContext = + vpxInit(/* disableLoopFilter= */ false, /* enableRowMultiThreadMode= */ false, threads); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } From 02ddfdc0c824a023463ef9b42cc22bc1b2b98d7b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 13:56:44 +0000 Subject: [PATCH 775/807] Bump targetSdkVersion to 29 for demo apps only PiperOrigin-RevId: 283324612 --- constants.gradle | 3 ++- demos/cast/build.gradle | 2 +- demos/main/build.gradle | 2 +- demos/main/src/main/AndroidManifest.xml | 1 + demos/surface/build.gradle | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/constants.gradle b/constants.gradle index decb25c666..65812e4274 100644 --- a/constants.gradle +++ b/constants.gradle @@ -16,7 +16,8 @@ project.ext { releaseVersion = '2.11.0' releaseVersionCode = 2011000 minSdkVersion = 16 - targetSdkVersion = 28 + appTargetSdkVersion = 29 + targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved compileSdkVersion = 29 dexmakerVersion = '2.21.0' mockitoVersion = '2.25.0' diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 69e8ddc52d..f9228e4b79 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { diff --git a/demos/main/build.gradle b/demos/main/build.gradle index d03d75f077..ab47b6de81 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 355ba43405..0240a377ac 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" + android:requestLegacyExternalStorage="true" android:name="com.google.android.exoplayer2.demo.DemoApplication" tools:ignore="UnusedAttribute"> diff --git a/demos/surface/build.gradle b/demos/surface/build.gradle index 1f653f160e..bff05901b5 100644 --- a/demos/surface/build.gradle +++ b/demos/surface/build.gradle @@ -26,7 +26,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion 29 - targetSdkVersion 29 + targetSdkVersion project.ext.appTargetSdkVersion } buildTypes { From b296b8d80744667ab502d729fda605f7e027f5e8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 13:58:12 +0000 Subject: [PATCH 776/807] Remove nullness blacklist for UI module PiperOrigin-RevId: 283324784 --- .../android/exoplayer2/util/Assertions.java | 36 +++++ .../exoplayer2/ui/PlayerControlView.java | 44 ++++-- .../android/exoplayer2/ui/PlayerView.java | 126 ++++++++++-------- 3 files changed, 138 insertions(+), 68 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java index 9a4891d329..0f3bbfa14d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -96,6 +96,42 @@ public final class Assertions { } } + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(); + } + return reference; + } + + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a string using {@link String#valueOf(Object)}. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + return reference; + } + /** * Throws {@link NullPointerException} if {@code reference} is 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 b8642e2e42..a6636d71be 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 @@ -233,18 +233,18 @@ public class PlayerControlView extends FrameLayout { private final ComponentListener componentListener; private final CopyOnWriteArrayList visibilityListeners; - private final View previousButton; - private final View nextButton; - private final View playButton; - private final View pauseButton; - private final View fastForwardButton; - private final View rewindButton; - private final ImageView repeatToggleButton; - private final ImageView shuffleButton; - private final View vrButton; - private final TextView durationView; - private final TextView positionView; - private final TimeBar timeBar; + @Nullable private final View previousButton; + @Nullable private final View nextButton; + @Nullable private final View playButton; + @Nullable private final View pauseButton; + @Nullable private final View fastForwardButton; + @Nullable private final View rewindButton; + @Nullable private final ImageView repeatToggleButton; + @Nullable private final ImageView shuffleButton; + @Nullable private final View vrButton; + @Nullable private final TextView durationView; + @Nullable private final TextView positionView; + @Nullable private final TimeBar timeBar; private final StringBuilder formatBuilder; private final Formatter formatter; private final Timeline.Period period; @@ -299,6 +299,11 @@ public class PlayerControlView extends FrameLayout { this(context, attrs, defStyleAttr, attrs); } + @SuppressWarnings({ + "nullness:argument.type.incompatible", + "nullness:method.invocation.invalid", + "nullness:methodref.receiver.bound.invalid" + }) public PlayerControlView( Context context, @Nullable AttributeSet attrs, @@ -350,7 +355,7 @@ public class PlayerControlView extends FrameLayout { updateProgressAction = this::updateProgress; hideAction = this::hide; - LayoutInflater.from(context).inflate(controllerLayoutId, this); + LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); TimeBar customTimeBar = findViewById(R.id.exo_progress); @@ -778,6 +783,8 @@ public class PlayerControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow) { return; } + + @Nullable Player player = this.player; boolean enableSeeking = false; boolean enablePrevious = false; boolean enableRewind = false; @@ -809,16 +816,20 @@ public class PlayerControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { return; } + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { repeatToggleButton.setVisibility(GONE); return; } + + @Nullable Player player = this.player; if (player == null) { setButtonEnabled(false, repeatToggleButton); repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); return; } + setButtonEnabled(true, repeatToggleButton); switch (player.getRepeatMode()) { case Player.REPEAT_MODE_OFF: @@ -843,6 +854,8 @@ public class PlayerControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { return; } + + @Nullable Player player = this.player; if (!showShuffleButton) { shuffleButton.setVisibility(GONE); } else if (player == null) { @@ -861,6 +874,7 @@ public class PlayerControlView extends FrameLayout { } private void updateTimeline() { + @Nullable Player player = this.player; if (player == null) { return; } @@ -935,6 +949,7 @@ public class PlayerControlView extends FrameLayout { return; } + @Nullable Player player = this.player; long position = 0; long bufferedPosition = 0; if (player != null) { @@ -985,7 +1000,7 @@ public class PlayerControlView extends FrameLayout { } } - private void setButtonEnabled(boolean enabled, View view) { + private void setButtonEnabled(boolean enabled, @Nullable View view) { if (view == null) { return; } @@ -1129,6 +1144,7 @@ public class PlayerControlView extends FrameLayout { */ public boolean dispatchMediaKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); + @Nullable Player player = this.player; if (player == null || !isHandledMediaKey(keyCode)) { return false; } 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 2e29dd3388..c55fe09f76 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 @@ -71,6 +71,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art @@ -280,19 +282,19 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4; // LINT.ThenChange(../../../../../../res/values/attrs.xml) + private final ComponentListener componentListener; @Nullable private final AspectRatioFrameLayout contentFrame; - private final View shutterView; + @Nullable private final View shutterView; @Nullable private final View surfaceView; - private final ImageView artworkView; - private final SubtitleView subtitleView; + @Nullable private final ImageView artworkView; + @Nullable private final SubtitleView subtitleView; @Nullable private final View bufferingView; @Nullable private final TextView errorMessageView; @Nullable private final PlayerControlView controller; - private final ComponentListener componentListener; @Nullable private final FrameLayout adOverlayFrameLayout; @Nullable private final FrameLayout overlayFrameLayout; - private Player player; + @Nullable private Player player; private boolean useController; @Nullable private PlayerControlView.VisibilityListener controllerVisibilityListener; private boolean useArtwork; @@ -318,9 +320,12 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider this(context, attrs, /* defStyleAttr= */ 0); } + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"}) public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + componentListener = new ComponentListener(); + if (isInEditMode()) { contentFrame = null; shutterView = null; @@ -330,7 +335,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider bufferingView = null; errorMessageView = null; controller = null; - componentListener = null; adOverlayFrameLayout = null; overlayFrameLayout = null; ImageView logo = new ImageView(context); @@ -385,7 +389,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } LayoutInflater.from(context).inflate(playerLayoutId, this); - componentListener = new ComponentListener(); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // Content frame. @@ -540,9 +543,10 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (this.player == player) { return; } - if (this.player != null) { - this.player.removeListener(componentListener); - Player.VideoComponent oldVideoComponent = this.player.getVideoComponent(); + @Nullable Player oldPlayer = this.player; + if (oldPlayer != null) { + oldPlayer.removeListener(componentListener); + @Nullable Player.VideoComponent oldVideoComponent = oldPlayer.getVideoComponent(); if (oldVideoComponent != null) { oldVideoComponent.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { @@ -555,13 +559,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); } } - Player.TextComponent oldTextComponent = this.player.getTextComponent(); + @Nullable Player.TextComponent oldTextComponent = oldPlayer.getTextComponent(); if (oldTextComponent != null) { oldTextComponent.removeTextOutput(componentListener); } } this.player = player; - if (useController) { + if (useController()) { controller.setPlayer(player); } if (subtitleView != null) { @@ -571,7 +575,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider updateErrorMessage(); updateForCurrentTrackSelections(/* isNewPlayer= */ true); if (player != null) { - Player.VideoComponent newVideoComponent = player.getVideoComponent(); + @Nullable Player.VideoComponent newVideoComponent = player.getVideoComponent(); if (newVideoComponent != null) { if (surfaceView instanceof TextureView) { newVideoComponent.setVideoTextureView((TextureView) surfaceView); @@ -585,7 +589,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } newVideoComponent.addVideoListener(componentListener); } - Player.TextComponent newTextComponent = player.getTextComponent(); + @Nullable Player.TextComponent newTextComponent = player.getTextComponent(); if (newTextComponent != null) { newTextComponent.addTextOutput(componentListener); } @@ -611,13 +615,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param resizeMode The {@link ResizeMode}. */ public void setResizeMode(@ResizeMode int resizeMode) { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); contentFrame.setResizeMode(resizeMode); } /** Returns the {@link ResizeMode}. */ public @ResizeMode int getResizeMode() { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); return contentFrame.getResizeMode(); } @@ -688,7 +692,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider return; } this.useController = useController; - if (useController) { + if (useController()) { controller.setPlayer(player); } else if (controller != null) { controller.hide(); @@ -793,9 +797,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider return super.dispatchKeyEvent(event); } - boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController; + boolean isDpadKey = isDpadKey(event.getKeyCode()); boolean handled = false; - if (isDpadAndUseController && !controller.isVisible()) { + if (isDpadKey && useController() && !controller.isVisible()) { // Handle the key event by showing the controller. maybeShowController(true); handled = true; @@ -804,7 +808,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // controller, or extend its show timeout if already visible. maybeShowController(true); handled = true; - } else if (isDpadAndUseController) { + } else if (isDpadKey && useController()) { // The key event wasn't handled, but we should extend the controller's show timeout. maybeShowController(true); } @@ -819,7 +823,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @return Whether the key event was handled. */ public boolean dispatchMediaKeyEvent(KeyEvent event) { - return useController && controller.dispatchMediaKeyEvent(event); + return useController() && controller.dispatchMediaKeyEvent(event); } /** Returns whether the controller is currently visible. */ @@ -865,7 +869,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * controller to remain visible indefinitely. */ public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); this.controllerShowTimeoutMs = controllerShowTimeoutMs; if (controller.isVisible()) { // Update the controller's timeout if necessary. @@ -884,7 +888,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. */ public void setControllerHideOnTouch(boolean controllerHideOnTouch) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); this.controllerHideOnTouch = controllerHideOnTouch; updateContentDescription(); } @@ -927,7 +931,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider */ public void setControllerVisibilityListener( @Nullable PlayerControlView.VisibilityListener listener) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); if (this.controllerVisibilityListener == listener) { return; } @@ -947,7 +951,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setPlaybackPreparer(playbackPreparer); } @@ -958,7 +962,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * DefaultControlDispatcher}. */ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setControlDispatcher(controlDispatcher); } @@ -969,7 +973,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * rewind button to be disabled. */ public void setRewindIncrementMs(int rewindMs) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setRewindIncrementMs(rewindMs); } @@ -980,7 +984,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * cause the fast forward button to be disabled. */ public void setFastForwardIncrementMs(int fastForwardMs) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setFastForwardIncrementMs(fastForwardMs); } @@ -990,7 +994,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. */ public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setRepeatToggleModes(repeatToggleModes); } @@ -1000,7 +1004,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param showShuffleButton Whether the shuffle button is shown. */ public void setShowShuffleButton(boolean showShuffleButton) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setShowShuffleButton(showShuffleButton); } @@ -1010,7 +1014,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param showMultiWindowTimeBar Whether to show all windows. */ public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); } @@ -1026,7 +1030,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider */ public void setExtraAdGroupMarkers( @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups); } @@ -1038,7 +1042,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider */ public void setAspectRatioListener( @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); contentFrame.setAspectRatioListener(listener); } @@ -1089,7 +1093,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTouchEvent(MotionEvent event) { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } switch (event.getAction()) { @@ -1116,7 +1120,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTrackballEvent(MotionEvent ev) { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } maybeShowController(true); @@ -1173,7 +1177,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public ViewGroup getAdViewGroup() { - return Assertions.checkNotNull( + return Assertions.checkStateNotNull( adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback"); } @@ -1191,8 +1195,26 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. + @EnsuresNonNullIf(expression = "controller", result = true) + private boolean useController() { + if (useController) { + Assertions.checkStateNotNull(controller); + return true; + } + return false; + } + + @EnsuresNonNullIf(expression = "artworkView", result = true) + private boolean useArtwork() { + if (useArtwork) { + Assertions.checkStateNotNull(artworkView); + return true; + } + return false; + } + private boolean toggleControllerVisibility() { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } if (!controller.isVisible()) { @@ -1208,7 +1230,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (isPlayingAd() && controllerHideDuringAds) { return; } - if (useController) { + if (useController()) { boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { @@ -1229,7 +1251,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private void showController(boolean showIndefinitely) { - if (!useController) { + if (!useController()) { return; } controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); @@ -1241,6 +1263,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private void updateForCurrentTrackSelections(boolean isNewPlayer) { + @Nullable Player player = this.player; if (player == null || player.getCurrentTrackGroups().isEmpty()) { if (!keepContentOnPlayerReset) { hideArtwork(); @@ -1267,12 +1290,12 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Video disabled so the shutter must be closed. closeShutter(); // Display artwork if enabled and available, else hide it. - if (useArtwork) { + if (useArtwork()) { for (int i = 0; i < selections.length; i++) { - TrackSelection selection = selections.get(i); + @Nullable TrackSelection selection = selections.get(i); if (selection != null) { for (int j = 0; j < selection.length(); j++) { - Metadata metadata = selection.getFormat(j).metadata; + @Nullable Metadata metadata = selection.getFormat(j).metadata; if (metadata != null && setArtworkFromMetadata(metadata)) { return; } @@ -1287,6 +1310,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider hideArtwork(); } + @RequiresNonNull("artworkView") private boolean setArtworkFromMetadata(Metadata metadata) { boolean isArtworkSet = false; int currentPictureType = PICTURE_TYPE_NOT_SET; @@ -1316,6 +1340,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider return isArtworkSet; } + @RequiresNonNull("artworkView") private boolean setDrawableArtwork(@Nullable Drawable drawable) { if (drawable != null) { int drawableWidth = drawable.getIntrinsicWidth(); @@ -1362,13 +1387,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider errorMessageView.setVisibility(View.VISIBLE); return; } - ExoPlaybackException error = null; - if (player != null - && player.getPlaybackState() == Player.STATE_IDLE - && errorMessageProvider != null) { - error = player.getPlaybackError(); - } - if (error != null) { + @Nullable ExoPlaybackException error = player != null ? player.getPlaybackError() : null; + if (error != null && errorMessageProvider != null) { CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second; errorMessageView.setText(errorMessage); errorMessageView.setVisibility(View.VISIBLE); @@ -1410,12 +1430,10 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** Applies a texture rotation to a {@link TextureView}. */ private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + Matrix transformMatrix = new Matrix(); float textureViewWidth = textureView.getWidth(); float textureViewHeight = textureView.getHeight(); - if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { - textureView.setTransform(null); - } else { - Matrix transformMatrix = new Matrix(); + if (textureViewWidth != 0 && textureViewHeight != 0 && textureViewRotation != 0) { float pivotX = textureViewWidth / 2; float pivotY = textureViewHeight / 2; transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); @@ -1429,8 +1447,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider textureViewHeight / rotatedTextureRect.height(), pivotX, pivotY); - textureView.setTransform(transformMatrix); } + textureView.setTransform(transformMatrix); } @SuppressLint("InlinedApi") From ab1d54d0acaadfa8b020ccf5a5bc3b32e6b2d716 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 4 Dec 2019 09:59:01 +0000 Subject: [PATCH 777/807] Merge pull request #6696 from phhusson:fix/nullable-selection-override PiperOrigin-RevId: 283347700 --- .../trackselection/DefaultTrackSelector.java | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) 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 0d74652408..437546559c 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 @@ -184,7 +184,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedRendererCapabilitiesIfNecessary; private int tunnelingAudioSessionId; - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; /** @@ -646,8 +647,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder setSelectionOverride( - int rendererIndex, TrackGroupArray groups, SelectionOverride override) { - Map overrides = selectionOverrides.get(rendererIndex); + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null) { overrides = new HashMap<>(); selectionOverrides.put(rendererIndex, overrides); @@ -669,7 +671,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null || !overrides.containsKey(groups)) { // Nothing to clear. return this; @@ -688,7 +691,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null || overrides.isEmpty()) { // Nothing to clear. return this; @@ -775,9 +779,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; } - private static SparseArray> cloneSelectionOverrides( - SparseArray> selectionOverrides) { - SparseArray> clone = new SparseArray<>(); + private static SparseArray> + cloneSelectionOverrides( + SparseArray> selectionOverrides) { + SparseArray> clone = + new SparseArray<>(); for (int i = 0; i < selectionOverrides.size(); i++) { clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); } @@ -962,7 +968,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final int tunnelingAudioSessionId; // Overrides - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; /* package */ Parameters( @@ -996,7 +1003,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedRendererCapabilitiesIfNecessary, int tunnelingAudioSessionId, // Overrides - SparseArray> selectionOverrides, + SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags) { super( preferredAudioLanguage, @@ -1087,7 +1094,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return Whether there is an override. */ public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); return overrides != null && overrides.containsKey(groups); } @@ -1100,7 +1108,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Nullable public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); return overrides != null ? overrides.get(groups) : null; } @@ -1233,17 +1242,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Static utility methods. - private static SparseArray> readSelectionOverrides( - Parcel in) { + private static SparseArray> + readSelectionOverrides(Parcel in) { int renderersWithOverridesCount = in.readInt(); - SparseArray> selectionOverrides = + SparseArray> selectionOverrides = new SparseArray<>(renderersWithOverridesCount); for (int i = 0; i < renderersWithOverridesCount; i++) { int rendererIndex = in.readInt(); int overrideCount = in.readInt(); - Map overrides = new HashMap<>(overrideCount); + Map overrides = + new HashMap<>(overrideCount); for (int j = 0; j < overrideCount; j++) { - TrackGroupArray trackGroups = in.readParcelable(TrackGroupArray.class.getClassLoader()); + TrackGroupArray trackGroups = + Assertions.checkNotNull(in.readParcelable(TrackGroupArray.class.getClassLoader())); + @Nullable SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader()); overrides.put(trackGroups, override); } @@ -1253,16 +1265,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static void writeSelectionOverridesToParcel( - Parcel dest, SparseArray> selectionOverrides) { + Parcel dest, + SparseArray> selectionOverrides) { int renderersWithOverridesCount = selectionOverrides.size(); dest.writeInt(renderersWithOverridesCount); for (int i = 0; i < renderersWithOverridesCount; i++) { int rendererIndex = selectionOverrides.keyAt(i); - Map overrides = selectionOverrides.valueAt(i); + Map overrides = + selectionOverrides.valueAt(i); int overrideCount = overrides.size(); dest.writeInt(rendererIndex); dest.writeInt(overrideCount); - for (Map.Entry override : overrides.entrySet()) { + for (Map.Entry override : + overrides.entrySet()) { dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0); dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0); } @@ -1285,8 +1300,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static boolean areSelectionOverridesEqual( - SparseArray> first, - SparseArray> second) { + SparseArray> first, + SparseArray> second) { int firstSize = first.size(); if (second.size() != firstSize) { return false; @@ -1303,13 +1318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static boolean areSelectionOverridesEqual( - Map first, - Map second) { + Map first, + Map second) { int firstSize = first.size(); if (second.size() != firstSize) { return false; } - for (Map.Entry firstEntry : first.entrySet()) { + for (Map.Entry firstEntry : + first.entrySet()) { TrackGroupArray key = firstEntry.getKey(); if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) { return false; @@ -1536,7 +1552,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public final void setSelectionOverride( - int rendererIndex, TrackGroupArray groups, SelectionOverride override) { + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); } From 92566323da68addfd64c2103d236e444a25b2d9b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Dec 2019 18:24:59 +0000 Subject: [PATCH 778/807] Remove some more core classes from nullness blacklist PiperOrigin-RevId: 283366568 --- .../android/exoplayer2/NoSampleRenderer.java | 9 ++- .../audio/AudioRendererEventListener.java | 33 ++++++----- .../exoplayer2/extractor/MpegAudioHeader.java | 4 +- .../extractor/wav/WavHeaderReader.java | 2 + .../metadata/MetadataDecoderFactory.java | 31 +++++----- .../text/SubtitleDecoderFactory.java | 57 ++++++++++--------- .../android/exoplayer2/upstream/Loader.java | 36 ++++++------ .../exoplayer2/upstream/cache/CacheUtil.java | 8 +-- .../cache/LeastRecentlyUsedCacheEvictor.java | 27 ++++----- .../upstream/cache/SimpleCacheSpan.java | 13 +++-- .../video/VideoRendererEventListener.java | 36 ++++++------ 11 files changed, 138 insertions(+), 118 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 894736571c..52bf4b3d06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not @@ -27,10 +28,10 @@ import java.io.IOException; */ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { - private RendererConfiguration configuration; + @MonotonicNonNull private RendererConfiguration configuration; private int index; private int state; - private SampleStream stream; + @Nullable private SampleStream stream; private boolean streamIsFinal; @Override @@ -285,8 +286,10 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // Methods to be called by subclasses. /** - * Returns the configuration set when the renderer was most recently enabled. + * Returns the configuration set when the renderer was most recently enabled, or {@code null} if + * the renderer has never been enabled. */ + @Nullable protected final RendererConfiguration getConfiguration() { return configuration; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 042738b4f6..bf5822caf6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; import androidx.annotation.Nullable; @@ -105,8 +107,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */ public void enabled(final DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(() -> listener.onAudioEnabled(decoderCounters)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters)); } } @@ -115,11 +117,12 @@ public interface AudioRendererEventListener { */ public void decoderInitialized(final String decoderName, final long initializedTimestampMs, final long initializationDurationMs) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onAudioDecoderInitialized( - decoderName, initializedTimestampMs, initializationDurationMs)); + castNonNull(listener) + .onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } @@ -127,8 +130,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ public void inputFormatChanged(final Format format) { - if (listener != null) { - handler.post(() -> listener.onAudioInputFormatChanged(format)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format)); } } @@ -137,9 +140,11 @@ public interface AudioRendererEventListener { */ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { - if (listener != null) { + if (handler != null) { handler.post( - () -> listener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); + () -> + castNonNull(listener) + .onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); } } @@ -148,11 +153,11 @@ public interface AudioRendererEventListener { */ public void disabled(final DecoderCounters counters) { counters.ensureUpdated(); - if (listener != null) { + if (handler != null) { handler.post( () -> { counters.ensureUpdated(); - listener.onAudioDisabled(counters); + castNonNull(listener).onAudioDisabled(counters); }); } } @@ -161,11 +166,9 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */ public void audioSessionId(final int audioSessionId) { - if (listener != null) { - handler.post(() -> listener.onAudioSessionId(audioSessionId)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId)); } } - } - } 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 e454bd51c8..8412b738bb 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; +import org.checkerframework.checker.nullness.qual.Nullable; /** * An MPEG audio frame header. @@ -195,7 +196,7 @@ public final class MpegAudioHeader { /** MPEG audio header version. */ public int version; /** The mime type. */ - public String mimeType; + @Nullable public String mimeType; /** Size of the frame associated with this header, in bytes. */ public int frameSize; /** Sample rate in samples per second. */ @@ -223,5 +224,4 @@ public final class MpegAudioHeader { this.bitrate = bitrate; this.samplesPerFrame = samplesPerFrame; } - } 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 bbcb75aa2d..97ce0c6a1e 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.wav; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.WavUtil; @@ -39,6 +40,7 @@ import java.io.IOException; * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a * supported WAV format. */ + @Nullable public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkNotNull(input); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index ae4b7db5c9..0b653830a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.icy.IcyDecoder; @@ -62,7 +63,7 @@ public interface MetadataDecoderFactory { @Override public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; + @Nullable String mimeType = format.sampleMimeType; return MimeTypes.APPLICATION_ID3.equals(mimeType) || MimeTypes.APPLICATION_EMSG.equals(mimeType) || MimeTypes.APPLICATION_SCTE35.equals(mimeType) @@ -71,19 +72,23 @@ public interface MetadataDecoderFactory { @Override public MetadataDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.APPLICATION_ID3: - return new Id3Decoder(); - case MimeTypes.APPLICATION_EMSG: - return new EventMessageDecoder(); - case MimeTypes.APPLICATION_SCTE35: - return new SpliceInfoDecoder(); - case MimeTypes.APPLICATION_ICY: - return new IcyDecoder(); - default: - throw new IllegalArgumentException( - "Attempted to create decoder for unsupported format"); + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.APPLICATION_ID3: + return new Id3Decoder(); + case MimeTypes.APPLICATION_EMSG: + return new EventMessageDecoder(); + case MimeTypes.APPLICATION_SCTE35: + return new SpliceInfoDecoder(); + case MimeTypes.APPLICATION_ICY: + return new IcyDecoder(); + default: + break; + } } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); } }; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index a64a1835d8..927ee8be5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; @@ -74,7 +75,7 @@ public interface SubtitleDecoderFactory { @Override public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; + @Nullable String mimeType = format.sampleMimeType; return MimeTypes.TEXT_VTT.equals(mimeType) || MimeTypes.TEXT_SSA.equals(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) @@ -90,32 +91,36 @@ public interface SubtitleDecoderFactory { @Override public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.TEXT_SSA: - return new SsaDecoder(format.initializationData); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel, format.initializationData); - case MimeTypes.APPLICATION_DVBSUBS: - return new DvbDecoder(format.initializationData); - case MimeTypes.APPLICATION_PGS: - return new PgsDecoder(); - default: - throw new IllegalArgumentException( - "Attempted to create decoder for unsupported format"); + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(mimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + return new Cea708Decoder(format.accessibilityChannel, format.initializationData); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); + default: + break; + } } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); } }; } 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 616859f047..a498f510dd 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 @@ -190,8 +190,8 @@ public final class Loader implements LoaderErrorThrower { private final ExecutorService downloadExecutorService; - private LoadTask currentTask; - private IOException fatalError; + @Nullable private LoadTask currentTask; + @Nullable private IOException fatalError; /** * @param threadName A name for the loader's thread. @@ -242,39 +242,34 @@ public final class Loader implements LoaderErrorThrower { */ public long startLoading( T loadable, Callback callback, int defaultMinRetryCount) { - Looper looper = Looper.myLooper(); - Assertions.checkState(looper != null); + Looper looper = Assertions.checkStateNotNull(Looper.myLooper()); fatalError = null; long startTimeMs = SystemClock.elapsedRealtime(); new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0); return startTimeMs; } - /** - * Returns whether the {@link Loader} is currently loading a {@link Loadable}. - */ + /** Returns whether the loader is currently loading. */ public boolean isLoading() { return currentTask != null; } /** - * Cancels the current load. This method should only be called when a load is in progress. + * Cancels the current load. + * + * @throws IllegalStateException If the loader is not currently loading. */ public void cancelLoading() { - currentTask.cancel(false); + Assertions.checkStateNotNull(currentTask).cancel(false); } - /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. - */ + /** Releases the loader. This method should be called when the loader is no longer required. */ public void release() { release(null); } /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. + * Releases the loader. This method should be called when the loader is no longer required. * * @param callback An optional callback to be called on the loading thread once the loader has * been released. @@ -325,10 +320,10 @@ public final class Loader implements LoaderErrorThrower { private final long startTimeMs; @Nullable private Loader.Callback callback; - private IOException currentError; + @Nullable private IOException currentError; private int errorCount; - private volatile Thread executorThread; + @Nullable private volatile Thread executorThread; private volatile boolean canceled; private volatile boolean released; @@ -368,6 +363,7 @@ public final class Loader implements LoaderErrorThrower { } else { canceled = true; loadable.cancelLoad(); + Thread executorThread = this.executorThread; if (executorThread != null) { executorThread.interrupt(); } @@ -375,7 +371,8 @@ public final class Loader implements LoaderErrorThrower { if (released) { finish(); long nowMs = SystemClock.elapsedRealtime(); - callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + Assertions.checkNotNull(callback) + .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); // If loading, this task will be referenced from a GC root (the loading thread) until // cancellation completes. The time taken for cancellation to complete depends on the // implementation of the Loadable that the task is loading. We null the callback reference @@ -450,6 +447,7 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); long durationMs = nowMs - startTimeMs; + Loader.Callback callback = Assertions.checkNotNull(this.callback); if (canceled) { callback.onLoadCanceled(loadable, nowMs, durationMs, false); return; @@ -492,7 +490,7 @@ public final class Loader implements LoaderErrorThrower { private void execute() { currentError = null; - downloadExecutorService.execute(currentTask); + downloadExecutorService.execute(Assertions.checkNotNull(currentTask)); } private void finish() { 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 93b00718ab..ce16ea2439 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 @@ -172,7 +172,7 @@ public final class CacheUtil { @Nullable CacheKeyFactory cacheKeyFactory, CacheDataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, @@ -268,11 +268,11 @@ public final class CacheUtil { long length, DataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, boolean isLastBlock, - AtomicBoolean isCanceled) + @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; long initialPositionOffset = positionOffset; @@ -392,7 +392,7 @@ public final class CacheUtil { .buildCacheKey(dataSpec); } - private static void throwExceptionIfInterruptedOrCancelled(AtomicBoolean isCanceled) + private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled) throws InterruptedException { if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) { throw new InterruptedException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java index 44a735f144..c88e2643d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -17,13 +17,10 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; -import java.util.Comparator; import java.util.TreeSet; -/** - * Evicts least recently used cache files first. - */ -public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator { +/** Evicts least recently used cache files first. */ +public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor { private final long maxBytes; private final TreeSet leastRecentlyUsed; @@ -32,7 +29,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar public LeastRecentlyUsedCacheEvictor(long maxBytes) { this.maxBytes = maxBytes; - this.leastRecentlyUsed = new TreeSet<>(this); + this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare); } @Override @@ -71,16 +68,6 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar onSpanAdded(cache, newSpan); } - @Override - public int compare(CacheSpan lhs, CacheSpan rhs) { - long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; - if (lastTouchTimestampDelta == 0) { - // Use the standard compareTo method as a tie-break. - return lhs.compareTo(rhs); - } - return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; - } - private void evictCache(Cache cache, long requiredSpace) { while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { try { @@ -91,4 +78,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } } + private static int compare(CacheSpan lhs, CacheSpan rhs) { + long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; + if (lastTouchTimestampDelta == 0) { + // Use the standard compareTo method as a tie-break. + return lhs.compareTo(rhs); + } + return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 7d9f0c9ff1..5f6ea338e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -116,10 +116,11 @@ import java.util.regex.Pattern; File file, long length, long lastTouchTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { - file = upgradeFile(file, index); - if (file == null) { + @Nullable File upgradedFile = upgradeFile(file, index); + if (upgradedFile == null) { return null; } + file = upgradedFile; name = file.getName(); } @@ -174,8 +175,12 @@ import java.util.regex.Pattern; key = matcher.group(1); // Keys were not escaped in version 1. } - File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), - Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + File newCacheFile = + getCacheFile( + Assertions.checkStateNotNull(file.getParentFile()), + index.assignIdForKey(key), + Long.parseLong(matcher.group(2)), + Long.parseLong(matcher.group(3))); if (!file.renameTo(newCacheFile)) { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 70f30d3280..e7dfd123b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.video; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; import android.view.Surface; @@ -126,33 +128,34 @@ public interface VideoRendererEventListener { /** Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}. */ public void enabled(DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(() -> listener.onVideoEnabled(decoderCounters)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoEnabled(decoderCounters)); } } /** Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}. */ public void decoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onVideoDecoderInitialized( - decoderName, initializedTimestampMs, initializationDurationMs)); + castNonNull(listener) + .onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */ public void inputFormatChanged(Format format) { - if (listener != null) { - handler.post(() -> listener.onVideoInputFormatChanged(format)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format)); } } /** Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public void droppedFrames(int droppedFrameCount, long elapsedMs) { - if (listener != null) { - handler.post(() -> listener.onDroppedFrames(droppedFrameCount, elapsedMs)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onDroppedFrames(droppedFrameCount, elapsedMs)); } } @@ -162,29 +165,30 @@ public interface VideoRendererEventListener { int height, final int unappliedRotationDegrees, final float pixelWidthHeightRatio) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onVideoSizeChanged( - width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); + castNonNull(listener) + .onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); } } /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */ public void renderedFirstFrame(@Nullable Surface surface) { - if (listener != null) { - handler.post(() -> listener.onRenderedFirstFrame(surface)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface)); } } /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */ public void disabled(DecoderCounters counters) { counters.ensureUpdated(); - if (listener != null) { + if (handler != null) { handler.post( () -> { counters.ensureUpdated(); - listener.onVideoDisabled(counters); + castNonNull(listener).onVideoDisabled(counters); }); } } From 668e8b12e06f896649ce53362c9ddf863e71e476 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 3 Dec 2019 11:39:03 +0000 Subject: [PATCH 779/807] Fix typo in DefaultTimeBar javadoc PiperOrigin-RevId: 283515315 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) 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 1efdeac84d..8b737bc006 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 @@ -126,35 +126,21 @@ import java.util.concurrent.CopyOnWriteArraySet; */ public class DefaultTimeBar extends View implements TimeBar { - /** - * Default height for the time bar, in dp. - */ + /** Default height for the time bar, in dp. */ public static final int DEFAULT_BAR_HEIGHT_DP = 4; - /** - * Default height for the touch target, in dp. - */ + /** Default height for the touch target, in dp. */ public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26; - /** - * Default width for ad markers, in dp. - */ + /** Default width for ad markers, in dp. */ public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4; - /** - * Default diameter for the scrubber when enabled, in dp. - */ + /** Default diameter for the scrubber when enabled, in dp. */ public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12; - /** - * Default diameter for the scrubber when disabled, in dp. - */ + /** Default diameter for the scrubber when disabled, in dp. */ public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0; - /** - * Default diameter for the scrubber when dragged, in dp. - */ + /** Default diameter for the scrubber when dragged, in dp. */ public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16; - /** - * Default color for the played portion of the time bar. - */ - public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; /** Default color for the played portion of the time bar. */ + public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; + /** Default color for the unplayed portion of the time bar. */ public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF; /** Default color for the buffered portion of the time bar. */ public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; @@ -165,19 +151,16 @@ public class DefaultTimeBar extends View implements TimeBar { /** Default color for played ad markers. */ public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00; - /** - * The threshold in dps above the bar at which touch events trigger fine scrub mode. - */ + /** The threshold in dps above the bar at which touch events trigger fine scrub mode. */ private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50; - /** - * The ratio by which times are reduced in fine scrub mode. - */ + /** The ratio by which times are reduced in fine scrub mode. */ private static final int FINE_SCRUB_RATIO = 3; /** * The time after which the scrubbing listener is notified that scrubbing has stopped after * performing an incremental scrub using key input. */ private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000; + private static final int DEFAULT_INCREMENT_COUNT = 20; /** From a6098bb9fa989760f83462174896705e1a302a22 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 3 Dec 2019 14:03:17 +0000 Subject: [PATCH 780/807] Allow AdtsExtractor to encounter EOF Fixes issue:#6700 sample_cbs_truncated.adts test file produced using `$ split -b 31795 sample_truncated.adts` to remove the last 10 bytes PiperOrigin-RevId: 283530136 --- RELEASENOTES.md | 2 + .../extractor/ts/AdtsExtractor.java | 64 +- .../test/assets/ts/sample_cbs_truncated.adts | Bin 0 -> 31795 bytes .../ts/sample_cbs_truncated.adts.0.dump | 627 ++++++++++++++++++ .../ts/sample_cbs_truncated.adts.1.dump | 427 ++++++++++++ .../ts/sample_cbs_truncated.adts.2.dump | 247 +++++++ .../ts/sample_cbs_truncated.adts.3.dump | 55 ++ .../ts/sample_cbs_truncated.adts.unklen.dump | 627 ++++++++++++++++++ .../extractor/ts/AdtsExtractorTest.java | 8 + 9 files changed, 2029 insertions(+), 28 deletions(-) create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.0.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.1.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.2.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.3.dump create mode 100644 library/core/src/test/assets/ts/sample_cbs_truncated.adts.unklen.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 373a024eea..d0ae1a3b5a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -81,6 +81,8 @@ ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Reconfigure audio sink when PCM encoding changes ([#6601](https://github.com/google/ExoPlayer/issues/6601)). + * Allow `AdtsExtractor` to encounter EoF when calculating average frame size + ([#6700](https://github.com/google/ExoPlayer/issues/6700)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. 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 381f19809b..5a0973188b 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 @@ -34,6 +34,7 @@ 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 java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -218,7 +219,7 @@ public final class AdtsExtractor implements Extractor { } scratch.skipBytes(3); int length = scratch.readSynchSafeInt(); - firstFramePosition += 10 + length; + firstFramePosition += ID3_HEADER_LENGTH + length; input.advancePeekPosition(length); } input.resetPeekPosition(); @@ -266,36 +267,43 @@ public final class AdtsExtractor implements Extractor { int numValidFrames = 0; long totalValidFramesSize = 0; - while (input.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { - scratch.setPosition(0); - int syncBytes = scratch.readUnsignedShort(); - if (!AdtsReader.isAdtsSyncWord(syncBytes)) { - // Invalid sync byte pattern. - // Constant bit-rate seeking will probably fail for this stream. - numValidFrames = 0; - break; - } else { - // Read the frame size. - if (!input.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { - break; - } - scratchBits.setPosition(14); - int currentFrameSize = scratchBits.readBits(13); - // Either the stream is malformed OR we're not parsing an ADTS stream. - if (currentFrameSize <= 6) { - hasCalculatedAverageFrameSize = true; - throw new ParserException("Malformed ADTS stream"); - } - totalValidFramesSize += currentFrameSize; - if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { - break; - } - if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + try { + while (input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + // Invalid sync byte pattern. + // Constant bit-rate seeking will probably fail for this stream. + numValidFrames = 0; break; + } else { + // Read the frame size. + if (!input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { + break; + } + scratchBits.setPosition(14); + int currentFrameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (currentFrameSize <= 6) { + hasCalculatedAverageFrameSize = true; + throw new ParserException("Malformed ADTS stream"); + } + totalValidFramesSize += currentFrameSize; + if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { + break; + } + if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + break; + } } } + } catch (EOFException e) { + // We reached the end of the input during a peekFully() or advancePeekPosition() operation. + // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally + // ExtractorInput would these operations to encounter end-of-input without throwing an + // exception [internal: b/145586657]. } input.resetPeekPosition(); if (numValidFrames > 0) { diff --git a/library/core/src/test/assets/ts/sample_cbs_truncated.adts b/library/core/src/test/assets/ts/sample_cbs_truncated.adts new file mode 100644 index 0000000000000000000000000000000000000000..5fe02a20c74d5380ef2fbadd6d0325c1542af673 GIT binary patch literal 31795 zcma%iWl$7;*exI+0)jNs(p^&0-64(C3ew#OEG!aI(j~b_cZW0rf^?U}lG5FC z``)=9@3&>xVL0>TInVjQPv1*EgFshMk&qm0&8^JLKGE=S@(Lj#A;%#jDZmlGMec+m z{Xq((3;h51LGu56jTit$f~CSf6_|(c15J?l?>m?MoD^`caKJZJ-p|xK1TVSbPN`&C zfS^G1!mxbT0KpT}^YPp3FDU1y9qwgU+5I)Q$$>rP(`y&-!&~txMI^3wJ(mFjqn%xQ zP#lt)YZy8>V%oLk1Svxt1j_4#!DAWu&u8y|0!U>1AFf@l&cE+$9rwArAaL{sw|-s( zTC-J&FB#7xIp#I>ORSxg`+Fc^k=x>EBm=Y6AAylcApYZ-*RP1_(NeyA%uzI_dx5I{ z@Y3CfT~&9wy`i<)lUA=Z)8y++XSXx_;5ehdZ0p_))XmcVtW6IGVdFUu7j5lzLoB%C|!$Jm-YSUxY(xeVh1S?IU1s+JW*snyqC zE3k5%nH9h=*-unCH_AH}N44_vv&Ry?o4F^_fk&)<4MC(*Od_gU1a2BS>J5hg!o{Xk zw?;g|s@Ns_Q55k6OTe4_FsDjMax+<^(!J-8fXF4F$f$(hxrwpSQjsl+4VZafg#^9f z4p*fWY*(P-EGVt=<{h4O#lUHvQU7wEGjepOXMOBs3aiS7lS{ODSl-OYqlE<=y)%i8 zwW6|T-;9kci|y$igqIf^9@%ffI>9L8%P}{$$M*dj&jiU7mM7JLH6~ta6@T!KMvdBN z-M0b=+2&3&y(oPhWTG3H^P&odpd}brb>gjG*^j$aw~RpCgEOIpYc#$GpD91=V_dAv z{P@1{qMy@J1Lp9p(DiwOcbDqUY^IgaB;(ByNzGq*&+U1Z!P8CC`#*q7Vq?SY zH-8Jd+kD+~8eK_VsNp=3=!h+#q{O@4T|2m9eEn=s_1RqP$4g*j>nXwS@@)xVYUKB% zSiH4<$2TU1=k{!KxeHr`yXq?ryZ+Xi!-l7;Jv(4NwwZo6W=w7dxsE%lroL*dgjG@iqci`sTS{vSe#EdZ-$i?{- zI1m&WiHMY2fDkPh#j@*rI4x;N2pxAgwWO$O!kgaQ+K<7u>`UpXobyTkVy z(A6-@dpLoI@Q#q5V~^NpPD{aH{f8Zc3(%dIAN#IXoJNZq$NgF7(UqTPluKRmY0`Bu zlD*#i8}(N=Eed9NcP~9coJ=x%m>vGL2-zGV|LQ7}7yS6saY>q>Q#&>??DC4^G`?iCjxE{n|OAf=|Y~-BV6j55&Q|KUwA|uLSS0pD!>G@I)GtJ10Tx@vTe>73;tQU;2d@xuQUn~3)o*5IZM^W7hne~6? zcz%@(hTMi_JJ?aSunxAA?cW5$9NeJx1Go~I>Fw^P7#@m=&)D`3=Ds@J0%IHRyUR}v~ul9AvCoTw5v|x8#_`+~l1}lt?+#Xe&@fO-$a1q;F|Wqn?l_JM>4Y_{v+d4H-*A=iHh5YRekA;=3I^m=$Ug zg1+&z!B8L)7lkN}TM&yFKB;(6b6LmeX-uX-@T}qVn zbXMJ5`-=oDcD`cp%vsFJvXDFG_x4!k8W8OOiX8;)-Ot@YGp$;$`U_gg2MU+P8hz@b zj6k`+19lr+j6s>sM_tU0EqpE=Nf}A?{({z)b%hJ#y!T+pyN-T{HVm>PdkvgRwKUoh zsBdhvQh2>ag*=5gt-ikn;!Qu5u2*NGMr^e z6)rg*W4vtPog|9c1zkuO zwdx&w`^4?=#Af+Qt1sK|e7OAno@)Wn#gH5MkP6DIpyQ-;NbI8W%crQ?8XAIDfzR5B z?m;W((ftFb@U}NLrh`114%2?Px|32ycsX{HI`1pX(4 zto)*3rpEUpk;}B0G1quyG5Z`rgP3*Ou~sUI;of1+-tB4-pLM9=8|ELSd@GTkbZdh? zu@JN~wf~mBQP%6x(5nyWsnI!bd77edC=&E>0U{?xT??%K$ZkXM0_BHPJnC0Gf`et- zN>6pRWE1>K`WhDepQ}`Y(PL1*l=@4tfXr5)Q!2E-apKG)o6W$1($PD835$FYcK!Fd zAk53-D%Cg7+he{h2sf={t4N3WqY&{svN#4}1M7{aKe=4?bO%U*=zM z@0a&yF3<#A+!^%t{TjmL7#O1c^YF>85_*yzZ&z@^tFu*g}Un0%VTu?%NB?pd62Z*8rTHv$>K zaW&RI9(9=7d2OT!xBf)$&;=QFO7yn5IIPz^Sf8gm{@$Bt`5>TSRVu=Ne@`#i<$(j8 zTl4z8f7mi4$fF|fFd4|uleD6G>mUNL_H@juB;uzHB&1s&ydV5?wHE-aW0flQGeZ4@ z)-HA}oS{KC1@*#G&3wCBX7CC-_bvBvd~AUPedXnY!BHlcZ`p(@t8nqvNe!hdEy*}P z(K<6qL?t`;2`6IApgx5Y9i=C*W|ch&KNRK18w^E8Ji6&cT?XYe=6KnkEi=6Gb&Cd+ zvEoDi67pf@kjtY(U9JR;&Y{UoF2QW`*bO;%JC$xBH>N((zOfH))AYBu2f+=%JnzU6)hIkp?9h zP3#YSPukg6JET6${U|~mCh{~mozQdN!~}s4bKOSH%}lK#SR~q-rEbLP_&*ussGp%d zC!uiQp(j+W5iE5HYgrW3s=#`sD2JVH!p4Q;smcs3SSPiDx4w6J<>|?BD%|)mX_Oi3u%|5D^jUxRD4Za$)kL(0@gDjU z9WH~9PG**x-?Rs3ExsveLB~z>lxUhXPO?DCQyH)$@|F@&wjX$fbYbvP?{tdGxl)Wv zq~ZX`WtVkL_Ltc+5ajn^#OVCvhahLyCOno}4wOOw;b-ByG}_3J0z;1j2+={bNBQU* z9_4Yo|H=~bNWU`Q6JsNgB8E%2IVNflJ$LxRXenmfSJ#%7WLrWlUZ_l@(( zzOfA4{WZ|sTxyfkiq@?c$nCSn)ZE3MM#y3ROE}G$xwv0G449u?3D~|j0nJK)3S+!A z*T#%5B8%(LJF%*-bZX%w{_dcw+@e0A7+;lo@I(Q>*6ZyKmK-LPjp94iwA0E&X|%u) zx8Tc;6AHq(D+@Z@6qR8UKAy{-v zsjF^^GNScp&0&zuTBNzme8|EqIt?}ArE_Aw+n(JFo1M6 z+9gYErGNz>lJVxxqWiKvi^p8dpAQnc1cTCi=pLKjG^*BHE7m1;VX{X0y7t@5Kw;_>%?E|1nS9Qyr5_|&*q z68}hsR}_!=iFV}9&4o$Cca9Id(ZjnBMK8Gy@uk*6p8Hz>adtE}e=TAbgBe zhclmjo942$Q_9C3?weXh=C3zc{*KjrUi;N&_&R6RSi&2>Pqt^V{#&+iN+7K∈f1 zT{6X+uap!9TcvXCyyk&OV%%uM-KlIVflqVvzx|%{alem3HhSKyx)M_z%VX#{jm<=7$*xYxDsq>4>NOy{)0&jkj=o{6d9 zsqlkRI|8*m{kI&XW_vu5QoWMxyQ2j#7l^4=WJxi1dVUK>;jspFX`TI7v*7y?@13!3 zY;k}H!{612B66zwZA+cP1+RPl4)ZtLGxNpkn`XWo#icVQ3?jc;VIH7xo^J!%M2Aq($LDg#iW)k8}zU?aRPtFHzUPZCJOI(!UmDILA9~@b*9k7eL zWWP&0{}+lwh`#RhSg}-yPK-(PsC(f(VnuwQN01sbnm^3=h(^VT6mB%*8`2R`zpQbRe1%i_Qc|{jbmzt!-IjwWA~A0zUz)jZyIY;x zjsZAh$oveSiXjIMXx%VNG?5RPxp%=45~3XRAw&+K&f5e=Slb($N7sL2Ei@1D@L~}o zWcIvO-FoW-dFvaGd*no69+rNoeA*B8d7-aevK0Lm^`JD`Sn|K=nZTO&%R4{c z&hM2qr;GQK;(u`WH8{WfTBol4EKApN*&S5=mF$(mwQ5q;(aOU}^Rl!v7tjY`bW?F8 z@pdphUfGYe2PN4~wmeHJ(lLIqCYMe8MdG+2&-a*G@^CdvA7mY%S5qn$CZW3*7|`pP zAD+l#+}vy-yF15VltFP8DWx)JAXp$P=&N;ExAf{f-#3np))_#B)$fy~M4((mqy?F5J> zlr&H%DpCrUX3vzTzD?dS?B|VN>31yh-_E5_Y6DBoAa-fRKX$K5V9?2=wG-1 zl|g%CGS|O+tl%3=!f9xq&rF>0=6GLw&1dF_6*NJ#G0HmTG{3-ZQ(G=IXY#z2`b7hX?;|aq2SJAjV6$DN{>v@I-q}}MWgD9 zdzwu?2Bp5et=Z$mbS$gA^CaQ86dqWu}{eM{7{wCkyj<(yf zv`CT!9kkO){xs=)3NIz-k=Vo`G$mRz>5n-d*=1SKP^1aP2eg6Cx}+vpO9_lM9xY56 z!J59cw`)UNdvkY-KkJYB>+(FQudfmpYPO)0i4uz-hyo@kR99;oqgQf-BbeP4d3VGOxer8$Eh%OJAP?4s{T^LS>K~BYSHns1?W9* z4FB8w(X`OoQ)BHusZvYXI&(XW;z}u&uG6&yZ#=C{DBGmXL|B|cYE;7##4j?S_JzzQ zU-gvv)Kz|JJfw*^X8zZ}Z2f#YM5GT(gReKCm|1@6V==|kUIBq3oePv`kmU?JKVB7Ft#O&1{+;&8eSVU1ZkJ+-`jMr#c|i3oEOCPQ4!jge@QP z&k8jR_`Qw3nH1d`iQKF)awg7nrY3U2MukNo9c}%`r&Scf{R&YRU84LD_T{u&-pkL0 zvC4B794)P1bCnqu!st8Yh0ETt7|x5WU?y~V4`<_K`>Seo>#I`_#LAnp30oH7Aoa4hKj`BJ1xUJ zhv2vE)cYV)gUpy=t^1A{yZ+Gcegft7UeI@aW9q`rOIZT*eu@j1J2pU(&LQ`%g;&Z4 zYFu_Rb-X8wT45`a8sC!Kx=#NYc%(FgrpkrK80JP#T07Dl@)n`$+Yp%Ua{^sl@>qX@ zZPoBw>pxbulCA{bk$%pt&5Jz+1C#(4jJP6 zQ$0o2S+U#yKL1D&i{TAHs$xP&`wuqGm+oR8|C3$dkFrZ9J1gOfFl6f=;pX@l;k~q8 zLy)W)6BE{aQ_e~yO_siYt|m4jRN{wsnNHcw+xDf~V|$4KV4M|WjCg1LW6LkxN~*oSwnGQ&1ufO_@T+j( zui?4i>9!2Ha?Qy}wlL#TK(syu)G@)k^c>ylBIi<2`EQoP#-Nv3zNs_d)ISGNS+f!u@`e2QNh03{Pxn(I|be#on$Z&C^Y;G*we*Rc!m}#;5uQ z7+C|KzZ3Kkefaz!zD(zlr3F*rfQu(J4PLqC?{zIM*{+>Jp^zxLg@=Gvo0E--hv&bb z3yxEwC+GVAz6CVd_9b(~-_*J7@3f;L%O#Yx+*NNk?rD1hhoDBzR_4nKj2ou`G1ibK zd9L1D3oPSn?Ve0_9@W)R+-;eg5pVAh){HM4epgt=4Rx=I#z3a3N{xMCww|{0>(O_j*&6e0?fye+q#r>mBiSSX&uje{J+f~B^c&(8 zm(NCd3p|wOeI{YCpZ2{s9N*S`dG{Sa}SD2D2F>d`34FhZCh~C6dUvvIV~eY_Gj}H; z0imQiDt>6a+{p^K@wQ2Sfb|8;_ct5y05s?ufm~~a8hwNhSDYpU`gOB-v=uTi#(PU54X<6;z zUmD9vP=Jyf>VI4Z{8*?ts*i`mUx)*ZDk(XF^gJqQgm`GEW^ck3rDd?XohvO@rntP* zRb>-Y{#3AR2_sJ{4c}fw7oR=AJB}(Jq9j(^G|094C2&oJOGBPZ)m|y^gI5LaSfLwv zmL=?M>Dp19Gq;md*r*meFglh8zte2(Gu*-tCGqV=q53Aep|7*PI7oyXfuLM15K1`jC-@5MdXp@_y536^F?`a}I4;iiaOeFU^x#ZQ`r? z7D=jKXuq^q^!*LgX};W~MIj6>NPp=~vYgE(Ss#R%QZPg8U;QNUQ%fLDZ@RCZj-_jl zDibLdq8CfYV@u;>AnwBcWd_9?tQPGvzZvaJZS~cgKFmqT=qsI08G_Qhp?tTV4&HCm z#YE-=)01Ma!b;-@hfsj)yxH@t_#NP77}x%MU+P#h2?#aqCtiv4q-X^*SNN zQNCVXQKH}gN68w1ZCD0P#-eCVt+>8_&RopWq=`9r-0Jg^e-XwfFA&mj|Jn5tlYyLVG9A! zj)#jvsbZfZZ|nUZBAfk1VwumPOqQ;?M`!Bc)ccfe_nUW%Lp?)bqLt9yTt7=+rW07z z+*FgobrxQ!vqgpn_I#s`CAiq;OxNILk=8JuXq9|SLf}{IGYvks`ba^*(81;Ty)XD) ziVAJVj>Z==}Pz}y$M*Ch!$BZszL5+g%e5-A)&#nYW3!d3<6^>)K|F%(l z+dUviU$xj+&r&FMR_fEkF~Bn*30YY^Bk(CDsHh*?IpV^OjTqGCUEjG0u@KU$%&Vuf z0b+!;$xZR|5~g`Asg!sYs3uf)Ad7mGv(>kk!u@dbxr}aZAKEhNKW`0!B%J>`Ic}e& zR1S+pA6;JGa-~Z8RP8alZsdQ4HEHS@;#Ke!1hltms(Rip8R}jtt3SXn5r-n!yZa*uRv_)j2w#G$qmsD7ifTgvsqX{(gFR`iVBhucV$F z6OWV_iSRo!DfuI9s`-czd0wLYk}Q5t&wx0SAR=!1uB~BZygb>H=uKCICds&X)f|5U zX#UE;QmB;ytIc8yH`2f| zl8b!E%Qq!fMWd~NfyFQ5$dR}g6?C10RQTsfi+~z(J7fYvBFKnd#)o-R<7~GY!1c2VW7>QE|2Y z%{|G*DGq?++Sc72oUHxyWP!co*x39x)a!7;e~X>oPA!Gk@-#Kjg$>_V_h0txw0mK6 zby;Z1AbNI(4E@FKUU3&q?nIv_@w21#e&2wyA4lROpe1J*in#yZ(&|BfnqCfl%Wp5; zEl&k^so;z8PK#8Xg2CbWe}NyC)GRjy_ldIFHV9S|x#idQd5(WDlK-GC;9|P%2CmYm zM$?l>*gUoP$1pz{V-TDIr8lsKhF$0}tr5l;i?;~t552p3a~gwkL`X)2%M7c^0M#_r zS7%7wb=nHd_rbVj@K)`g3;fUtjWtRKY3C;N4eRI{BO*5BHeJW%GuRz@Y{?h{nz93i>A08Tt z43R{^uRnHvMTH$)zdT7!4aH>0^_2&LK7ZkfmKT9nRkefz&MeLYh=ve}^ePVutJv5{El zWm($&`K7gUr@Bd#mto+I^BnbB@a3(_YoEzBpjMHA;zTNX#r2qfw2Vt`Boi$& ztDG}D)s7%5Vr7}NbE9p?xOnz7k|@DXQWgR#F=`)zQ?ScoAr>;4VnteQjOZ&N=a_(Z z<&kBw%QJfg+vLLkOkqed0Qlaiml%7eK+KrJ&_%W?u!XR7I2-wPY-Ve@l0C5ytA}%a z|MKGCcK?3rudNBDFe{;Y#9<}uh~zC%h#;rH};#nsm zzU=C)?Nyd6>Xt1QuxG^6ra_z2m}TD59h^w!L2AqNc5>M>Wd~B8l9v+xBvd{g=R+Uu zB`)+>_$a?*UVWp-6G`U(z_5P$Sv6rcL^s)c#)euNp0~6Gz4mWy2SYA@I(80w2$d`w z{yuqgyAGhQ8>Y-w0d|1*>})TAI7xr2$VXG19A(ZVu3_Grsh-P>)@yTCG3zOS>xXR( za<-+FCA&ka!I~H5DoF|73F4a?H^0jgT7Z8n+)xMmM~bgsFMRX7ugad?nSZTWsctze zIcBD=mkF$Ji8~wA>~1WtA-!sRIh<}>$BjD@duPBO?Y~1qZ1QuLzFH}Wcfqi#iWp78 z{X#S81H%gzm$t_L=;GthMM9{Q%mg?Hl@hUZGh^XN2hrpxS|M)r-3VhM*uKEeU3Rp! zEpk@3ax%YQxaFKi-*7{ty7yp0<3i!rLV>g1L;Kyph)EucRaxCGlfHN*n1i|W7w^HD zO3~auozB^oI^>1n<6iUtTlXO}!-s4E=K^|imK~;jQ{T3?N7T9FlSck6*CU>yo8vcH zg5Rz#=zrNO(^8a)!9df`5E|{dS-?sFZ)MEK<^rJqIp- zG0jq>Dt;!P|A|qQwz2siG}nP&n(F7)QnNxd`KawkG;dsI@&EI>Q6W+po9Md>BL!jv zAvAYxgs0})56R*~0*G?Obh`|1xNNPt^a}6? z)cz>gsOQ+d_;7B%-)hO01xc9`9D|h{B(@aR@>~EV;LaQ5o)6W~9jC$9JImXnmZweM zw67{f{Y%E$^A8W!7AcqNdic_L%7i6M`rNN38y2Hkh^F<`W0HJ^tFF_DB{M$%Hqz&m z9iyEcy&sMTN4l!y-JiFo;8eLv6zOi1f~_)yzGA#g{1J>l3CoC0G4vzJjUV~(_E#0J zpVe|3S9U+ynAt1+dgOUU?T-JaQz14=!pE`uYo1veA-VscDVD?ay*l+NHI-e`nbnDVP0@aukodZBRe( z@FXSX*dExK-xG4jk?X&5MemiE2Lx1BR?lXen%-SDRW_pPHsu+=Y%bT0kH=w+zH!)8 zK&B|uMVhc}b9;5{L8rWn&2L*(^~J+^z+;EVgd59!4(We6BtnY*&dnHtfQqPIMQuz} zX)~G#Rj$eO0l)oZgP{ti{MItg^O{6KnyjB!GybnJhLBL?lor9EFR`{SBgxkk)45dmFT{HZMj9yO z+Vi+Ow<<OYLYm=C<~~^eRN0zk&vzU z;QYm0#ly?_a@h6X-VAsgMETq#*e`5Vgb^s zA*8}KTil~76YQdy+GZ%HBr?cmk-6l0@`B*Od4G0Sn&=ekcDDpOM(?Wb6J_ZZ)FGHW zvaZE}SNFMcBx7`wk#0cG^GrS_-sI>uu`EurutD!XKIGZd ziC1tj{9*T>cnNx(ya+25R%jS<+@n4~@Dfw%eK^8}@^+;%^KGF4`|J8u^^&eA|cu;1{%3(r`V#AKX~v7wMouc5s&^A3$sL!PDqeQQl0EO3mY zHNLp5CdF`~QE8@1H6cGcLmC*^&dR{tvDkXohT?bMsG^T}8&F-{7WuKi%vRH!MC#Ob z#~Jr78i&+$6Z}~bA`E)zlVgz(FnQ6g270MMOWdORFXJPQQ>$g3Z^=HE&ke=TO9;Qo zFgiBngAN^QJM)6m@vjvkA4iU@LQZQgv@c)&?<9J1Aqye)1X0gLB#SX>I?>G*RdMGIa%;wJ03s2SG z?mw(Flut}~gPR&Zh``H0UWsgiJ}Xz}w@gRag9E(7%>$|jE>08@LmastM+L<_MXgT2 zR!8(y%g-P`o7>b=iF^DbW#q2e7ALXe1AQS6D|U_U0;9j9z03)&3RrpJjdHbZqQ*dT znZS(uuOyw3YQ9-Q%oBdk+D)_sFkJgiO!?H50FmdV?_Ug|WB&V%OX1zWV+S=iBuO zJl;P+$)H*vHVLcL7wvDPjv;dTsUK(f^P6Jdp-hzT9WEH%!zk6CWBfG{=rh4-cXKT^ zalx{fEh;LXi6XzaqK#G~693j77J2<4@EoffT$C`RFnun46x4C8kcYu zaPU6-eDNT4Zx6QyRKS;wngUvdZ#_SoUER0})$^H+Q(m17RXXYVwCa=?TF>+h#9 zcPK7saeabGW{N>jU~d<2Y0@X&)3CSQWHV8>|C6^JjN;kX{TFmhCG7L?S&~qGTHx>N zZhzi~;fVRS9R=CL?8>J1>_#Co&}avGR~6;u=4WJX2~1BH1?ne^e9Q=4_L1lym@I<8 zN^!rJ)fCxcu3%+Qevy*OKtJHg3ViQ0*_=AnIcIk5+bI-qC!}S$zdLgrkm==hP^Sij z3RwQeW!U|2>-*R1o7@b*KeKJIE@R|f9TvhcP#Cq#;iPWRpbZc-Zr4{S($Huc9Nb_0 zU6Qutm@w|8%N2HJf_#FD8yb$A2Au<)@MkDnv0wiPZSfBYdD%&AG%) zIqrH!z2=9#?oX%`pU%%u*_TAnC)e`a@8?!-8I(?t_~!zXpLNsQ%y_uo>maIc1SJPW^%T zfvOs_oh0sW&ny@EXP_1)&i!j+mmag=Z3I(07&U^4?1;KuG;99$bR=PVHU$1k^;(UT zQsV9dcT=DL8k5u@Q(X`LDskB^rMMs0K?P%tiHG;6i>(gdSW680yD_RIYoY7oU0hZv z)V|S;ASI9MzgkVH3`KrrUbeoaFSLKjL$y)KAIp|DQ8SO@DzzGl0H=W#^bsuu{6Dn~fi~iChVJlUVh1scm{{zyvdEfP zq~cc)*=qj{g!wH|`{S$(!GkY+ed^$#fKDALl3u`6&u6oZn6c|C{wjSiHB%L?!lLuO zgnZQ+--%13#wPUyB-c1Lmg>H+p3m^F53p6_fSp7mG0cidL2Tu0ra!<(QI22gtKQ}0 z$e&AYBoeH0K2md2PyJXoH36{!8%}`)ZGBijbaqSOEZ@Rf5zD^zYTrEtceh$2E0zQH z2}g3(lb_WuKKO0>5WZ)b%w0ii}VA9g)10 z2!2!hDnwHzeZ$dnS!gMrfJDo<1XUS`bsu#B%Tj4AhTPw_THRcK-I3aen^YR0kzU=jV69-AMaVqv7V0X34*$ z3x>Ab?oz_4;-`QpsWHNOA>$-HR9Ic~tv8IdKe`?R_J{=G@Z)=p6&muzD#xIhmNXyr zjo;Rve$uw$5?0Fw)}dUiJ0ghoJ>;MM5@0G;2RwagS)^o#U3RlrhBb4vB;=*d_~FRj z?ar+`I-o*$FGF2$tg#GCoxBBrLB=u2M?f7;WetvMRXJdjnD=nB>b{w84pB|dvGh=F zRRX)=_sx{!<-gz9f`uPY;QS?-WzA z8PIckDmUo6&9FA2E74W5z9IecgoFfCqckLk*} zabHcpH!rJ_P;psdE!#ToZqCE=v(&oOUjZ6>5R+-hAn0^WT>HjeB^Ok6wyb}70Rk7j zR||L1m(3Sy&{U86`H}PU1nNUu-Au||kRsk@eZU|VsfMybzrr1Lgs_9HNM!Nl{(48o zyl$->vo5kQfz^1$T!o{0%cpl*kxM3#{m=MyzUk4~OY5@-g)qw}E5-xeF(-RbCGnq} z!Q>K9WI`euK2icalwQf&_w0nP(NLsb#i=3`f>wr^k!AZdPtDhEnZ)Rh#Ig=R*8!D=%UcHnu#S=p^dsTF zzp+IY_OfG}<`pQ}#4e3a51W!fetSEgCyE_&!)GB%qQMb9g)EdxDm_~$KkAa+QiHc% zMZLO|;|#DGRdM9YpD(Drys(mU2-WEu0k_-O43J}AO4_M2<-c((l^4-eE2F9MEEMH( zbwig#E&h*4Ab?>!IhG!k%Ic2}2!yD=q+a#X351PpSv^A>Y2Fezfr>2mlXRD zK%ylvHRC;e+FLhot#qTYG_(FmYVY{gA+bvRL#!vDv*o@H1GRne?b$(iN80CI8a+Vd z3zEI zty~RfZKl@d^uLqCZpBP^fS?Mlu=h7Ug0~JZQ480iaisED*j_*bm9}rIude@HTeBCi z<;s`yazr&NIK^@EMBhmwe9$@H39SFl@Au)e=&v&O?Q;|l-^f>9p;jUTt6ooXjoUBf zyxR3$l-$*aa5~$#jhJ(RwmU^EfB6`H43@uS?0bu0sh@cw-Cbs1)}Bgj^$x(I5|U9( znMVW>#+p~-;nQc=zN7h%J%OM|XN2DoR$&BcMCa!w=0kjtew9Se!*;h;o{aO>E&nxR zJnI&H>D74I_RisGEj++~mg;z4llln$+7s&Md@+~OK{fX0^8f+tFdy5LU8L+Vx^$pm zs|6q!eAKznKWyY@xzSy52C-;ihc)Kb`!YSWDVK|7H>Z1X;sxmB1v2dBZKZl9rIcYO znUa4OWdoINud*J+T)efLf^e`M(0N?H=5d!WU=-WBfHih9M?Ld8IJ&M8t{fCC=%`IlW5BG&nBE8Py|x7Q36t@;B<=^qFT+qpA&hTm4=@A*PV zyX!uBKj_p|h*r=i^Wj9I@g0>JC5d!))fZHTk#rIfS9r-!&~_z24|VqGI8M??Q1_3Ax6&)a6fid~foFT_R3 za%q6kgl_ox?=hI^yhFsnkG$MjI{kB~0(HS#PoK)r%+69Kzl*t@?&L$@>y``e@S?)K zbCuLE%GXN1Alh+WE#fGLO#NSZk~+WyKgZo&%R3u+62{R8dV`i4(ya;>8;U6F5foq* z8!Rq6h7d*Onta6R{+=?NzyD5nJ7~_`r@?WM{Q3dg2lcajJg!QXKC0;lC4QwViU?Ef z0(MuTI&XTic~?pPl;Y%|$|iZN-(Q5Cw6nN=k1EX;493@^&BoOQK9w-&5$$&uJ%n8Q z>`~8SBzr0SrBdBcZNQjz9PO@|Jz# z^lU5U-PPVEM|K-D$q%<>d7Uq0;*;b^bOF^Qo@=QnL25MbnzKW=!H6srI^f*VQTkx) zvhgKkj<*QIk_`5>*&{ab@D7BtVsyJ##1V&VA~LF%JLz6#GB;5zb9LzH{mq-I8m$3W zhmmZR3Cp%@2bjk%^p`>XW#oW?6*)uz3WJ{8-{~4O7B_8#j$2%#)AdP_Rf{;PNgWt!+$-jAlA%AJ?c)EMI z=CSyJk>0KnKUH}@@;OY`x}huh5}r;q59*-ue^?eeyO2!khL&JKU_DLAt3Un#qZ`uO8YaO4sv(Z(AHPM_Xyg8LDzs5-lg7w@ z%4}-tC)puSD8rfYNxucr1*D&D`;?~lFqjzO*?(1G&>f@W9D&ST z-oii*L(~wfgtANsAhE8|zo8;bZ6%lQ~u&Bjm^GlZBBG! zl1+5m40-#iXt{x_(2Fz0p;Z7^c1}F<=U$U4-ssl>UYjR7hB^zzf*1{IJd2Lu8mBM`KrdZpuT}e8NR1L{==;e$ z2EI7~r%dfn%HM?)uzHx{QV+Gh<%V#|XYYvr6TJC$@N_|>0X$VS;#&G2?rMZ#JQVqT z_4^QdjF1qsSKnnhEp_*MCsO`x+0|AQe?9bWCivr5Axz%(-7i+H0Os4)d8E4Eu{=5M zl_1PC|7l^XEOxf}^kfkZAJgDEZ>bP}E02F`vfC`|i9=mCu82?xNN{vTk&qd$y_p0cu--pKUWj~{S zGiz=BqG3nvRW+=3)<;DVCUnI)%8xnsROQs*$3dNmcoXn43=Kie9%olLVo)oF(qVme>esVbaZn(KWgTdESrPr=8uu-#OR0 z&ULQ)!Jnqg>3ujv%MBh6QGK%0$8bdpbvlW2Qd2MXIHfw`9-Hv6qwJK;@ZcSLLr2;pc-%T4QKd3dMHkqeQip%B4jCWu{h-^)HJRLK0j9u9Oz$HU@u z=y$2F)w2#MD{YWj;GH+Gng{DVTSE0PZjH<+4gYikm zSFyZSlFm+fs!zH8yE;lPXh>`Pz`T;aJ=$t&En~OfYF$@cmb%%_8OENIKC1jqn1Djo zko*>-0Ar~ehL=ZB1wRxU$Z@@V1UR37btZ6u@z%@Ot#n;QD)*mO=?Na)-{~^4CnOt+ z;m63y2qXw*qu)8&aMs^3+<3VqU$U$h#dunI=yCxPuvHeAs(bo`JS1pw=TNt#7=eGb z&-#$JlgcmP2_ecfV2&;>r*cYhrDRy^iWw{vd2y7OeRrwhWMj5;RaDL(i>LM7!{zXj zYo`V@w6oRt?6TsC9f>57^#T(Ww?VuuYoK|>-q0WMD6@HIV(CquCI%^O#RJ`|;a+i; zjCZVp@_J})Hld#*moJ~Dublmvaj_BxEN|^|BC$ffvh;)L@ERO9WQ+6X(CL0gI6ccpcn`W9zlB8ZnyoB?B=C#+f%ai)@YjagoAccwXxo#;5X9B6@Gx2P`LvF((7=(+~ zgRWa5&{*uCjMt9}BY3jfsV8`pTT)V=Bi~bLhnf_sLY)PzDeC+hq3?61(m7uFCC_#6bdj14J8)|W(SaeoiyX-ukmXG(=Ur_1!>B=ydShQ8{7>!vd0{WI0s_3%Vq1 zTo^w``!1_S|KXFo(A+t+>9zjQd^(jJ>0d}CHsi@6kU>WkMKvM=YHtd+A2Tp zA}Jkbwki>m5DZW3;!js8%DFq5FC7;5>t7QIX>=n^Y_}u`RsblyI)O(b{_pBcyMLk_@KVb zqw~`^>Y)Q0=MW(GvcAt65{pESf{RvQAXYK+}WZ;Kn_; zQX%DI>q%PC41{=Gk=}7>79+IW_Y3Lgxl*Iz9Ok~{^n^b$ys9fYt#q;+^)ZG7k3+~P zqf8)D|0k*^xLK4SeF|8{@ceb;2PSaR8o6P+_T%0yAzBrc*7J5+2TTI1AwTJQdUh^t zSM5g^S5D3`67wR1E0EdAgHhi#%k@|RhwToO5Vh9&3-a&+(XU*?M}e4y`pNPE z(7UJp?!6TDnsk(=u(yR=`5X_K2U&$^V%@(pj>{MN-PN9&ErVuNeU-6v)gU+wM&+_e z+QjUFmLJ_+>s*UgLW-v_+bE(wIB*}J`sF*GL98ESjdn=S6>BR*)Y+HJX;ZU1sxsD# z{jErhmy;xec$ia0=@0ntye}`Z924zwv#)sn{+^u z5;1AG^n%Y0+ltC3SySny72RKDC5T3%xvG}k2GFD$3c(Epse6JzG@{1y1+NW!jJ=Ta z2t_CT_2aLwc>Y;X_E}II|MODPzIXM<@h(n+?!}jL2V0Ks(uQVzHHvbkc0Y~HM0Ctp z!9{1-;H-WdQ%}{(-SB+ll*#`%%4K~L{Cz=JyobO4B@I4;P}=g9!fnmdtKm_W>fP;+ zt1jO5RQhW29Tx#>cuT&!C#-e#ej0KPy_b6MWa>107scIHCu%lTdp??3^8qT^m4<^D z$Zv8MIWZ5eBT@W#$0UfT6WNA!Tace!|D#HPUIU;?Zpd)}iUHU=nMI2-90$;em^)c8 z@s}~_74H_>Gy5`B1PZl*HX{PPqLI7P{qWKU2y^|+Dunbx>Hc}7s5|F%Jj;1gn3O*k z#d*MF&o`JZve&1noUQaCzthWGTUv!U^re`d2Zwom5%)6fOjl zqZr2|8m3~W{72iqgGaog9I&fy~jy zs{+e{?&M(~(>8qmM+pVUM}Q$Ez@5n8Mqpb4t6xAu?*JCcW$Yj0#U83Uo2oPwkvwir z)G*YREbj$zRW(3!z#xc=es@qr%>0yVXH`7Rsz428Eu=3o;)AXkpR{ndS^pFN(!5~_ zx}dtYWd6WzuPLx~h10JgChRn>>e4N{*^W>vT>zZH&%Kk}`XPc&QVT|v6bt4oeO+Ph zv{N0N)nX1=QcH~Zf@erSqC5Hi?{GFBOQpbGdDQQ0Mxm%@Y7&)4>iCk;`KL_XDdL?E zRz`n)-?%GZFue4ug)5F=R($oXRorx@ZOBNvg}7T)ex`s@JL)6uu0e^$KTU=CEf%}M z+&694jh)9sfEG-^kt4NdqL;43o0U4 z1S-Qq>+T(W8YnJwas4ykg8p;Rn$6)EY~XlYn@44&+c$vousrg%Gcb#JMWYIB*Y4ixNSF{f^H7`1FqYMt!@}H(MO;1NFK?k`J36Fw6rd8Gs>FC z=D!OUXuF(UI}I8nQwtiWqm!LOUTnsjfQS5dFD7O@53exQE*qdQ?+BEBs=C+pmM0!^y08@BhuQ?YQzGPJLu-?D4y+4Xw84Qn-gs@ z;)nHFxLVOx|L}caYk*yNvvwn<`*r``c&Hd6B!;Q>TGt{v*(LEIYM z7CZBSFH-q=dW0e~6C$&QqA%pZ<>|V`pM4vPj&MtdJ45SWHV&bvc{&>uYe)_{E^N)) zbrtOSGd|^FQswNalU(lB)tI^K*PxI9S@B`%uL*}QLbZ;4o!TWFGzZIlub8{)?IFmv zlW#Ihhm~xV`{SflShL^2hl#)9ma1;h$`c@~;oK0w9RGSN4RR&Ckl^Q0CZ`qAWfP19 zb-HQxO{S)DjkX{wr&MQw9d7vW{K?U2AQ!4fLkB~u9-%Gn`?H~%->ZKez7=pfzx4B3 z4_fx-cUOVTPY&v|&K*Io7CSguxB3Y8>YML&f{0joixkM8{4KR!WlsLrT?F00P$0~kSs$=c zZ%fVxxPobX`6WZl-ciT*)k4=@d0w%sZtRix&>ggTI*}P>>l88Wv_qGu21QW)^cnE2 z#rq3}Z$G>ebSvNd+_wZZZl^q z)NJ4o-Fe3i-a#+hAJ#c5<1~v+p0FVFNauk+!C;fj$(E^!Wunww zEcK8hN$PX_bQ8la%OJV^MlAl3%>d1}cG(?u{`W zKkg^f6J}nin!4iev}hz`zNV@-E>foZ$M{QuH2`%-q!BV!3GY5#k>G}7~+(azw zI;f_J)zUOpwlt*#2ZlmMKh=Aa1$`J$%jLC+WNTLIrPJPi5cJ{N?1Z;h?R2!|{_cYJ z!+CZhucpn|qNevwxbN;CyZcZO?XZoaRsvsKd5S7yyLR88C!t8!#?E4aMmMZZFi>DnR3ZAz%+Mmkt8+j_IzrPH$CoP zO8k+m5Cs3MmHTZlT#G zp9pS=7D;$s?tJ?GQ0co-*i#Nel0Ns-O2R)yyFVfcQ;j+p#Qs@!(9i)b(Blk1GY#;P zKz>nR#DAc1emLYhRAb1F_gR-Aah1_@vGUID{PH}k=3%qL&iV9K!8V^?S#2A0dDJAA z5znC)#EWQVE2*Tv3KiM%`BDNo&P03$`(y2B`f+460O1MaXb%dUL0z6~PfzhOpUTDh zHP;>GB0^0?S$`+={qoDgYl-P&(Gh)8=A=x~@b)tMuKvm2xny5-SF@Tv_EN0%O3d#7 z`~-{H@A_mo1H^05aWoK*i{Zak9>oOnpu0{#9<3hmqaw}K2DQ`}cw*~;slRCg-;2B;bLT|5h{zop#jmAZag)sZ48P1(qQ{2hQ6uyDhBj2jC-h~_ zicZCaO=%q*xd$xADMLoSEVF{g4F!Gy8-tBpO z0zA40WRF>HeS?6?uK@u3ked;xv?X4waf(r425%25NuoYUne15M$^GeTis4Hei!sux zMTi%zICR#N>j@a7KG7_pw9tu+_wf-@#SA)A*d8hdg`$1!l?CZN)t+3plr_&XJ?C^U zN&fuk&FFfj$d(lX8h}R^p?p@pV%x|1lJ94IwtVRHoxriA9Qt)U_nwU}i4Ko`|5~1R zH1qwG`WT{(OkHqHhtEAEkT&?&!Ba4?_ zmDzw@Je(2Zvtp_<3@XQCIYmq9~UcjN4_D*U)fF9Wzc(M^79 zenvkyrQAwS`ew#9!PTs!{Kf4$$uG6^gWc@vM@9oVQn-yrnU$3ETfdaj1~in}_TYFl z9kvil<5^lhx)(C6L{4%$-~`eMa6$o|R0}sXj3-w?Is$5Aay&OKUHvku(8NKwSG3GuPo1B5-r!{&t$iG(Aj~qj$oJ&l{GKmL0CSN-` zuzY6p(~B+%=5=+2M^?A}y}5SOz_kNdGfqGNdQV#FOE{4S#7l3!Osh&)@daSKmYCy6 z+oNW1jkz*)vjyAeM+9|LHJG_uZ>)vC)Q0WM_Sgg*Y}EYMXV~mK4xzF}rLC4!7N&Vx0+F z;ZI8v9Z1w)3jYNR>O98~P0_6HRUQ$L$yxA?m95pxqlOzd%Bz+((|kH!=Ug9&3_f~X z*sI>dxwYCktJm*TV=9=AN@w&yeW z!d^#jFFa%-yWKx3d^ud0|BZtqGWmZV7vkn|J?0U_56=fOM=}E`KAz4#Or*y?!PL8@ zJc&(VWRp;LAvaLfYkJC&1iXKNzN!uoT^)e1p6sBeXxMX{nz$0p@3^4LSysTEPu_Z= z&C}A>z=>N9`In9`u+xUu9h9GVwn0(db@taF?ZWHoWzcmqyu;ji)=qLX2b{8Y5Ol3N zN|5B?{jB<)*?z|;aLV)( zG2DD|A(PJ`x}s|GV-qG?f^{&U_6m-V;P5?En?dz-+euVMdd~_pQ8{L;^!2*uG-~i|($FBc`nTd_EB)wehy}6a2b}@z zB57Uw+Oz~VBy(+hOVwFu9`oA33yi;z-%D=Q*<~U~{;rN6<3~#36#_LDVhAeqYr+-%Fw`Z-4_co z6)lYe_Jv9#R39RwuAh>eXdO~rz<%wYvn!u28?RL%hVaMd#0mRPw-IZLwdguiF7KU` zAdsY|uBYhaVsG|q6$u~_c zqJD~p7dJh7RB8EpA_Q|d0{V0z8|=t%ZaZlcus|1(=XtGfX7mkKUnWrIb=iuBsb7rS z)Nltl9bdiTF?(*r-ft)FyA;JS5)mHI;MjH0z?rp~0vdK$nXjj0USxVG;Qbq%&D)Ea zrNFN&C6#+0`uPX&a;Gln1Wbp=vucEmSI&>IJ_z7mG_ z5|_OuLP1jezQ9==$Uj+B?bx$NP)zOJ;bCCmKxo#rECZpm4Fk@mkJ*5m2F*`!Q z#4RCFSJytjB1wsY1?Gr*x(i;MFIn7gG8?4!KJ~ELJF#i=@H9=`_@0tR^laoa;!Vc+ z{OsnL@+ho5Fo0ebJtpb0_`1Z7;^~0d*=f(aveh+D=y;9SQoN`e4XTIzdbgfcf z4oIEz%){>%_a`iOd2PRYEXZ2SNxA6}-)pR7&E%9}G)KmCTfTtE0FLnS&AL^`=qnZt zC6Y&B9nAi#DG^kBy3)U66*cj``AHq8U0@`kRq8fgqabUj{+V$BUgt2Uxf$)OXt)@> zR`Nq2CO+-($&&d=G{5oqbmzmXbnO4cebe0sw0w4x@I+77m_7NlF^VApO^!(vS%mBR zekjA$p&a!q)TZ$Vj}FQ6 z)@#%azqfRTFoy`8p7Ygm7cvz`zA8RSR(@ht&umPcpXvuac-9iehkeT<+$bOMZl;t^ zaO3Y-O93x0po|P*_H8mLP17n>)Yo<6(n+E&gnTq@Th+x}p=&QKMm$_j>5l`eHr@p_ zpgY-E>$7+N7Ecy5*m7mNTO?eSj5Ci``(L23rT3a?z|w;VpCcus{)?VZ2>aPpy12?? zeICu&*(G}dv*3G=jUdy7OoWH54C(88L4CM8m&qP*Wk!+{#{8}*#gXZsvB#VqZ7Gs) z(t_NjK82!^)<+g8RUdYi>V%*4zkhZ~QY9jCtm<>v#Fx4DqXT&=%*kuXqVnn%1ydt# z9vQ%Y#o&Glc^(PSUSfw8uJwxgQ7O;08Wr_5CS(q9x8RFRa~EVKXAW=}m?+%a%a>HI z`Tf=j{?*!OD^K~*f=dNV#Q(_C_#i31BVb@s*VyZN8vPf%6NBdfvDWWB@*C^Yi`mgz zG@D(+b5V7p2s}M#Fn;x+i`|c4|3e&j9(DH$thIoKp&bPfpm}Z3UYf;z_NP$GZ>^u@ zXbhnCYn!PT5YFlvd2R}?uQ9u9TfGD$_?^PL@`3hwqF1x*`Wbh*92XgJIOHt?HnthdFB22gc2|PCDGC3;Z4@QSa2lDydqqF!sZzX6h zrxz7Wbd=G;GWyjaWV6Ie8RWT^A}^=uk!!~NC#d%I5Jo6NumEvA+HOe^H!-r-&O7+W4 z7it&5ue7nJKmNOG;9G854LFg&CRnaPB;TXRkQdKB)Ag8iF%(x9E3#8pu_>jcX};VH zu64R<73(YTdKW*dXCCf;2@3|FKb?Z3ntvQAm=)%}qn-Em)=Tp)(S3Ps}c5N88YloH=`+M-r8YW5OOER;@^ zmwXm}c~6cnhubIGl2Qy*cfsD3)cw`4$}44U`%UycyEkmC;uB$ajkz79i)zh3s=ZjA%JDX@^RWG|iW{EhjnDD$hi zyj+Qkz5njHLy!8-@6aXqqP&^8dXdEV#$Hd4lY}2gA5-ahhyYiA)+ksq@I2f4Q=-Vw z{~dWa^CTK9ff8 z9iKCwOP$AmHW7YDLUEvi9{dZHJTemY|Dri3ue_G<{Uz}tPHlQABh`J2pK#6MQj5a` z53u)V+bX~n`nlXOnyFHHMG<@8mcP1z_(0$CgHQNe*J=yVGX7th)nhBZUT5bMced7Dq0C!aEG4F`Q-Q#RT)KVFr!PHs)sart6NOm^s;f-t%KwM@uj`HBh=mJlEOTCvq}t)P zof@7|hHIzguc0wKFUqgHKi<^+tEaDXQAMc3SLN)w$!J`p9Ttyu4Z6r4t zT3cCt>jW9uhdb*HgOB_%9RDS>#@&8}DCYgBkW}hn<7KC4h99pz+7!2L{4~S0xwC|g z#e64hQm7kapr=lnoK-3VvtTn5al7MX)#i*0!7-&x_EE8k;4m1PzkZ#WVC?zB$izdmv&;{SRww0XM=o5GqHJ ztH0gUa@^cIJVPKGhG%17#0WFDRWVqE{kNoP->f-Y*l&BS(mjBxHEDz#vDC@ zh`CRS$8qDod+$sgQI=93(^JjlFVH1z8RZ3tH)k?*FU4nSy>!mzRG`>N9GIOt`%!tF zGCw(-t)5&tOVID(qjuyty1Y_;}K< zNGWBW5fiQu>uc%E6@5!7rBBn;+&8(OR-=igvpL5st`u}j_N^sda89g1I%8~1TFS}> zMRT|WEm5mIo6Vz=@uEb?H)3Z_X(c7Dn$ixif>C47>8D5ZU72Ffg@`<2{TCbRqcY#P z6kb>Tq^u-WV|}ANm?d}`$6hlVELM{$WA}ycw zTGHcXA1pNfmR0&CD{ZR$EHOR`|7<5~p!}?&H7KU#aCmR4X4Pq9Iy^f8jxJ`ul$Y)p z6u&MPSi?*g_#M3pvS8ghc;7(uR(YXrIJt>jJ3WlWR=EtdAnNVg?mmQR1nWP_t{~jQ z7eoE7@--`3`f&Htkye2YD(u7SJ(Sb$AFco=u{ zx&Q!sw50fr+ZZ!ianl=_(lZnw->SN7UcDVqw@&o)JNB@il>vJk&2m-2f7&gKuS`C( zH6DG=YgAffpS9zhFP|*;lT+~wN*g7UUL1_g#5)R*HzQ@o`zMz0z#PI zockAQdUe;;%T@iI8|@3NHf;(n(t~$t!atoR9AaGtjN>l)vTdD+O(RO;d9=v$0P0bD5h49tK&yi0SC1dF8& zi-Q0^<}_cGlea3#1G`kVDl^o4ik?sW-fX68{k5iOH>;$--m0P=+Dhi9jzA2Ri1Uw~ zw%|4_=~HP&87(5Rh8H_u{;<5q!i(9syna~+yI=XIqD%_wuJ*t_^fV=i!Zr0778X`u zR>9$m*9be;7EUg`n=kHOURHQ77VMSi;$0iLHRu1~zAb)Y~ua20~}o|2`#G zhN!aQ-xgdd;HOgqKi!%|=QH0~;VX_eq2iN?%nh4)RUu3LJ0atff#RGacB;fhAbhf6 zTHVEG9Hx7T<@|(;FB?T-Gf-;O&f12=DRi9`98If98~&gD%FW)mrDKI3efZ2&QKshG zCMPv=N|?Hsr-AQ8N~4PV3#UzZA2DodwDmz#jBAs=QW|25d^0PNyw(~vS@llUlyCCu zKIHzXr&>|@k9J+Pt9x%4^D?o7*AL%kdvr1+7bpqlO7u9;(N1+gTysy{G0^;Kp!n}l zfdRqFI4}l#fEyL^E;n)ss9>dK__}O`DXog+mINM|mr$=~{or z^n`%xr;B@I5`S(Y4Ff<1-uLx7X_YL@-Y~Y_EkG16b-W$){p%hIIi0AlWT2n%#`#23 zcRtM)NrF$-=KkI5g~-C>{5Xy-S;uB~hBuoWZ%9HhiM1tG5D`Vxv;X5b@yNb#;t}F@ zbV)z@#X_vyl)jbi65CfhkX$s=r^Qe(ICY_FKH0Fi8bE1}8V)_1uQsDt=T4ct43KP1 zc0)o8tUE{dOrNTFo*y*GoM>2rfLEhT*&RVu-0?4!tw~#t^7b3VFR^za&Y4z8A{Wga z7Zo+$<6&WUnY!s(X1At-l{Q$sRUtVMlc?QAADknF#c4@WS&CF<_udN6AR>1c?glcZ z{!;tE2G!(P(KC)ZD4v{8wCqC7(5P%XidEHN>q%utq~bR+L*Y9u|5_=!8vq0lg7g4z zwa88X2H0SKqJI`eS6u3MxKO>tNUll0oA*q7j^d&SQ$1Iu2O4^+zIi-+ea~4qZXtsC z^Mi#}5cSp3tbFtO>UmP#Md1oqQG$^35r%v6TAvUNo<5RPZd0%EDu9c4b!j!&BCZZMZ^l>B`c%u#1Pj+DX?amnS2IQY|t*@b$XYZ%qI@7)id%aDvVPC6AN6)l>2 z@tb5t!JP}HHf8sGN*~qXT2|D+l5TiKl_SLb*quTjRJRJ~g!+?0^Z`GFt_HBCLIT1x@C> ztrg&rX%u!loojlZV*;ktZ5W%{=eCx>c4t^}LBCv@1|rL_n=Ky}8S}R${(4_*{|NTB zseoV3FoU*DzO%L2x?A37#Yfopgh-)KllnEUMPnbH8JpgzY_-1JBePOgdoAKQMepwM zJ4(0lJv9et3&6KfkHsXPFFDrKSh`1ol4rqR!b;MoEX%D2J@j;Sgdk>6B9VeT5+-N8 zPd(8Tu@a1?i=dY+*=Q9M;L0!S4}{cJ4K!G)W!JxLNY-YtR_QRE>#J+3bUOK7S<#6{bje-M7k--uk*kP4C$`CVT4$k@i= zO4si*d+YDU7R>IE7I(7tMmYN}fjUNxbF1G@t{W9UOk`+xbL-A zVW6fORiE*4;=#vPxlCk*skM%+YI3CLCNVB+s<(cENRFuo&iucJY7yqwHImQ;XkX@* zQM%F#_R_z>IZuPQWsEQOJ+v7W)52R7vXkYznu)3p-aA}_bp@vLCGmuaX zYTE8fw@YC${KFYEU;sgl0V*#JfDW(_zH#QmzXvqVUzmtl!=z71>b`!MjD}hkS{IG? zC*b;g_njU6_7*XX{Dhd?@`Pgcz|erFGv`C@L5ycEaX~atc7g_vzkWlZDv>=3v%cT@ z{R`21{Xb_&W6la#Y8Q$Z^3}bx@Nv6wY__kO%tcA{$YUAi58%U>+zEi+eHruxi|9ig zJ8=`~ua`)zv>>oMt*#JZ;R3G@>uYt6W3MPZHxCKzqCUQ|HIUPP1OjPSpo27)Jp0L3 zquejG9H~c3{_KyQO0mEF2Y7E{0~BmbY`0nvz{?9zZpx6ge6Z6hGBwj;WaEB?*Aj1c z=R$nJNd@s3D$#s?yxs|)k2qdXAE)7=p@F(D z)^M8SUc=8pJpF~2s`eXOFk{4@Pd-MN0=U&uA-Cu;|dhXw{*_Hps8 zDp+>q#i)6vfJlYEKRk{6H-rEJ1LTJ9ndpI?Z=k2#BM0945ezut(!a+KKdG2lEjKPN zQ!1b6n$e96oy|3af4HvuGwKN59YAu-3QixDl?*SALfAmd??v=yI7${mpF+hNa_CSa zBm0waq?pnqE-}ZPbAq@mCy3O{;b^QX#w|%KGVnbebw?2j+llB!A{w| z(Toa6aTIOXo<|cbX+-C_ZIZzY{Zs9|z9o%5!jTnW+({^YQpo!8s_>3H8Cf<}c#}q9 zU7oi>$;&4kf2V+V-uKRX;9p;y`4ak(cS#EbRKF#d~6G89*qhz0?IHRy^BGkjSsGAY`RM0 zRrv=DB@?#HdlA>~NBytnM=lAEGHt;7S18}pv2aWcjQk;5_4(`tcPVdbKM{Q0RP=q- znmg}AN@)Ffj=bUtnck<=IOX3Vu3BPVCi=92Y(^BO6x-Wofr3I|JK7ZVNwrl1Y8o&6 zlEM!T>q1B=6G^@sXt5V^8)6SfeSC(U2dMFY|Bwp!4@qtWT;jNi!Irtbmi5+YCcrL| zmyFhqWfxM@%Qd}1%1pd3dL$A#*37$yyxscY{03Ym>WKL%vZVr30LOcb; zBbpyXJtIRXX5Fqo-B;Xq>EC)9&UhBl;r%j-!pupHP%yk0G*WUn?yi~qTlT?XzUj?8 zpHWrC&sUa0Eo=mf;hG9~2LHAFL$22Vxu8p_K(2!u6AZipgzYggj?AmaB44*$CwcY5 zO_@yXiexHv<s|)tC6$^kBcp2*l*eF^P2jaT$nUi} zD|;n{ujosNxO5QQmj8JJ2gzUI8C!@%e_fuC+zCY zlo+26NDT8Uwv|o)rxKA(duaY;d`WPBo0rnQOr?~RaI)-iu+@KmUwrgTJ^4>(jgmse zQ=x-fYeMLr9+{7=O=;itz1W~rmMSU(EG)_gFRu5uB3}H)93reeDhoanCy-!au_gOL z<>BWh_i`j8|BFihokvX1fZxP&x9Z%(=kky*H+2AL5NT#LKqclubPdIHl5>{THcCS= zZMWf)1FXYk3g&GFR|*q7GWc3-;_Ag5CVmbb;a6FBKWPtahT7k}P?s2pxgT@rD@hT$ zd|h;8_ff*8+gCTxZvHZew(NfRL7f7{BqgSg*6x#8>3 z-F7!6FO`n@DY?;43lL59l2bQdFBUb~F8}C2C1>-2! new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), "ts/sample_cbs.adts"); } + + // https://github.com/google/ExoPlayer/issues/6700 + @Test + public void testSample_withSeekingAndTruncatedFile() throws Exception { + ExtractorAsserts.assertBehavior( + () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), + "ts/sample_cbs_truncated.adts"); + } } From ab016ebd8126d4298e159b67d51b790ce26e49d1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 3 Dec 2019 15:47:10 +0000 Subject: [PATCH 781/807] Fix comment typo PiperOrigin-RevId: 283543456 --- .../google/android/exoplayer2/extractor/ts/AdtsExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5a0973188b..86dacd8c30 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 @@ -302,7 +302,7 @@ public final class AdtsExtractor implements Extractor { } catch (EOFException e) { // We reached the end of the input during a peekFully() or advancePeekPosition() operation. // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally - // ExtractorInput would these operations to encounter end-of-input without throwing an + // ExtractorInput would allow these operations to encounter end-of-input without throwing an // exception [internal: b/145586657]. } input.resetPeekPosition(); From 9e238eb6d365e76b20e39147aeae5091ea4451ee Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Dec 2019 16:05:27 +0000 Subject: [PATCH 782/807] MediaSessionConnector: Support ACTION_SET_CAPTIONING_ENABLED PiperOrigin-RevId: 283546707 --- RELEASENOTES.md | 2 + .../mediasession/MediaSessionConnector.java | 54 +++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d0ae1a3b5a..1911d32cdd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -131,6 +131,8 @@ of the extension after this change, following the instructions in the extension's readme. * Opus extension: Update to use NDK r20. +* MediaSession extension: Make media session connector dispatch + `ACTION_SET_CAPTIONING_ENABLED`. * GVR extension: This extension is now deprecated. * Demo apps (TODO: update links to point to r2.11.0 tag): * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/surface) 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 84d5fea0c7..5382e286a1 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 @@ -339,6 +339,21 @@ public final class MediaSessionConnector { void onSetRating(Player player, RatingCompat rating, Bundle extras); } + /** Handles requests for enabling or disabling captions. */ + public interface CaptionCallback extends CommandReceiver { + + /** See {@link MediaSessionCompat.Callback#onSetCaptioningEnabled(boolean)}. */ + void onSetCaptioningEnabled(Player player, boolean enabled); + + /** + * Returns whether the media currently being played has captions. + * + *

        This method is called each time the media session playback state needs to be updated and + * published upon a player state change. + */ + boolean hasCaptions(Player player); + } + /** Handles a media button event. */ public interface MediaButtonEventHandler { /** @@ -420,6 +435,7 @@ public final class MediaSessionConnector { @Nullable private QueueNavigator queueNavigator; @Nullable private QueueEditor queueEditor; @Nullable private RatingCallback ratingCallback; + @Nullable private CaptionCallback captionCallback; @Nullable private MediaButtonEventHandler mediaButtonEventHandler; private long enabledPlaybackActions; @@ -606,7 +622,7 @@ public final class MediaSessionConnector { * * @param ratingCallback The rating callback. */ - public void setRatingCallback(RatingCallback ratingCallback) { + public void setRatingCallback(@Nullable RatingCallback ratingCallback) { if (this.ratingCallback != ratingCallback) { unregisterCommandReceiver(this.ratingCallback); this.ratingCallback = ratingCallback; @@ -614,6 +630,19 @@ public final class MediaSessionConnector { } } + /** + * Sets the {@link CaptionCallback} to handle requests to enable or disable captions. + * + * @param captionCallback The caption callback. + */ + public void setCaptionCallback(@Nullable CaptionCallback captionCallback) { + if (this.captionCallback != captionCallback) { + unregisterCommandReceiver(this.captionCallback); + this.captionCallback = captionCallback; + registerCommandReceiver(this.captionCallback); + } + } + /** * Sets a custom error on the session. * @@ -843,12 +872,14 @@ public final class MediaSessionConnector { boolean enableRewind = false; boolean enableFastForward = false; boolean enableSetRating = false; + boolean enableSetCaptioningEnabled = false; Timeline timeline = player.getCurrentTimeline(); if (!timeline.isEmpty() && !player.isPlayingAd()) { enableSeeking = player.isCurrentWindowSeekable(); enableRewind = enableSeeking && rewindMs > 0; enableFastForward = enableSeeking && fastForwardMs > 0; - enableSetRating = true; + enableSetRating = ratingCallback != null; + enableSetCaptioningEnabled = captionCallback != null && captionCallback.hasCaptions(player); } long playbackActions = BASE_PLAYBACK_ACTIONS; @@ -868,9 +899,12 @@ public final class MediaSessionConnector { actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player)); } - if (ratingCallback != null && enableSetRating) { + if (enableSetRating) { actions |= PlaybackStateCompat.ACTION_SET_RATING; } + if (enableSetCaptioningEnabled) { + actions |= PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; + } return actions; } @@ -901,6 +935,13 @@ public final class MediaSessionConnector { return player != null && ratingCallback != null; } + @EnsuresNonNullIf( + result = true, + expression = {"player", "captionCallback"}) + private boolean canDispatchSetCaptioningEnabled() { + return player != null && captionCallback != null; + } + @EnsuresNonNullIf( result = true, expression = {"player", "queueEditor"}) @@ -1353,6 +1394,13 @@ public final class MediaSessionConnector { } } + @Override + public void onSetCaptioningEnabled(boolean enabled) { + if (canDispatchSetCaptioningEnabled()) { + captionCallback.onSetCaptioningEnabled(player, enabled); + } + } + @Override public boolean onMediaButtonEvent(Intent mediaButtonEvent) { boolean isHandled = From b112ae000c9b377569fb19a782213bf6c750ec5c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 3 Dec 2019 16:51:43 +0000 Subject: [PATCH 783/807] Use peak rather than average bitrate for HLS This is a minor change ahead of merging a full variant of https://github.com/google/ExoPlayer/pull/6706, to make re-buffers less likely. Also remove variable substitution when parsing AVERAGE-BANDWIDTH (it's not required for integer attributes) PiperOrigin-RevId: 283554106 --- RELEASENOTES.md | 8 +++++--- .../source/hls/playlist/HlsPlaylistParser.java | 16 ++++++++++------ .../playlist/HlsMasterPlaylistParserTest.java | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1911d32cdd..0574445600 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -105,9 +105,11 @@ fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)). * DASH: Support negative @r values in segment timelines ([#1787](https://github.com/google/ExoPlayer/issues/1787)). -* HLS: Fix issue where streams could get stuck in an infinite buffering state - after a postroll ad - ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* HLS: + * Use peak bitrate rather than average bitrate for adaptive track selection. + * Fix issue where streams could get stuck in an infinite buffering state + after a postroll ad + ([#6314](https://github.com/google/ExoPlayer/issues/6314)). * AV1 extension: * New in this release. The AV1 extension allows use of the [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index ffabbece97..993ce8e5c1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -304,12 +304,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = masterPlaylist.variants; assertThat(variants.get(0).format.bitrate).isEqualTo(1280000); - assertThat(variants.get(1).format.bitrate).isEqualTo(1270000); + assertThat(variants.get(1).format.bitrate).isEqualTo(1280000); } @Test From e5957912452ef099be8855ba2b58995c8b31e833 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 3 Dec 2019 17:18:27 +0000 Subject: [PATCH 784/807] Clarify Cue.DIMEN_UNSET is also used for size PiperOrigin-RevId: 283559073 --- .../src/main/java/com/google/android/exoplayer2/text/Cue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5d763ca72..bd617ad626 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 @@ -32,7 +32,7 @@ public class Cue { /** The empty cue. */ public static final Cue EMPTY = new Cue(""); - /** An unset position or width. */ + /** An unset position, width or size. */ // Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero. public static final float DIMEN_UNSET = -Float.MAX_VALUE; From 5171a4bf5ed152fff38f8734eef46579035e7e29 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 3 Dec 2019 17:42:48 +0000 Subject: [PATCH 785/807] reduce number of notification updates Issue: #6657 PiperOrigin-RevId: 283563218 --- .../ui/PlayerNotificationManager.java | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) 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 569fc93456..6c77284e46 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 @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; @@ -52,7 +53,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A notification manager to start, update and cancel a media style notification reflecting the @@ -269,14 +269,7 @@ public class PlayerNotificationManager { */ public void onBitmap(final Bitmap bitmap) { if (bitmap != null) { - mainHandler.post( - () -> { - if (player != null - && notificationTag == currentNotificationTag - && isNotificationStarted) { - startOrUpdateNotification(bitmap); - } - }); + postUpdateNotificationBitmap(bitmap, notificationTag); } } } @@ -303,6 +296,11 @@ public class PlayerNotificationManager { */ private static final String ACTION_DISMISS = "com.google.android.exoplayer.dismiss"; + // Internal messages. + + private static final int MSG_START_OR_UPDATE_NOTIFICATION = 0; + private static final int MSG_UPDATE_NOTIFICATION_BITMAP = 1; + /** * Visibility of notification on the lock screen. One of {@link * NotificationCompat#VISIBILITY_PRIVATE}, {@link NotificationCompat#VISIBILITY_PUBLIC} or {@link @@ -598,7 +596,10 @@ public class PlayerNotificationManager { controlDispatcher = new DefaultControlDispatcher(); window = new Timeline.Window(); instanceId = instanceIdCounter++; - mainHandler = new Handler(Looper.getMainLooper()); + //noinspection Convert2MethodRef + mainHandler = + Util.createHandler( + Looper.getMainLooper(), msg -> PlayerNotificationManager.this.handleMessage(msg)); notificationManager = NotificationManagerCompat.from(context); playerListener = new PlayerListener(); notificationBroadcastReceiver = new NotificationBroadcastReceiver(); @@ -662,7 +663,7 @@ public class PlayerNotificationManager { this.player = player; if (player != null) { player.addListener(playerListener); - startOrUpdateNotification(); + postStartOrUpdateNotification(); } } @@ -945,26 +946,17 @@ public class PlayerNotificationManager { /** Forces an update of the notification if already started. */ public void invalidate() { - if (isNotificationStarted && player != null) { - startOrUpdateNotification(); + if (isNotificationStarted) { + postStartOrUpdateNotification(); } } - @Nullable - private Notification startOrUpdateNotification() { - Assertions.checkNotNull(this.player); - return startOrUpdateNotification(/* bitmap= */ null); - } - - @RequiresNonNull("player") - @Nullable - private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { - Player player = this.player; + private void startOrUpdateNotification(Player player, @Nullable Bitmap bitmap) { boolean ongoing = getOngoing(player); builder = createNotification(player, builder, ongoing, bitmap); if (builder == null) { stopNotification(/* dismissedByUser= */ false); - return null; + return; } Notification notification = builder.build(); notificationManager.notify(notificationId, notification); @@ -975,16 +967,16 @@ public class PlayerNotificationManager { notificationListener.onNotificationStarted(notificationId, notification); } } - NotificationListener listener = notificationListener; + @Nullable NotificationListener listener = notificationListener; if (listener != null) { listener.onNotificationPosted(notificationId, notification, ongoing); } - return notification; } private void stopNotification(boolean dismissedByUser) { if (isNotificationStarted) { isNotificationStarted = false; + mainHandler.removeMessages(MSG_START_OR_UPDATE_NOTIFICATION); notificationManager.cancel(notificationId); context.unregisterReceiver(notificationBroadcastReceiver); if (notificationListener != null) { @@ -1261,6 +1253,37 @@ public class PlayerNotificationManager { && player.getPlayWhenReady(); } + private void postStartOrUpdateNotification() { + if (!mainHandler.hasMessages(MSG_START_OR_UPDATE_NOTIFICATION)) { + mainHandler.sendEmptyMessage(MSG_START_OR_UPDATE_NOTIFICATION); + } + } + + private void postUpdateNotificationBitmap(Bitmap bitmap, int notificationTag) { + mainHandler + .obtainMessage( + MSG_UPDATE_NOTIFICATION_BITMAP, notificationTag, C.INDEX_UNSET /* ignored */, bitmap) + .sendToTarget(); + } + + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_START_OR_UPDATE_NOTIFICATION: + if (player != null) { + startOrUpdateNotification(player, /* bitmap= */ null); + } + break; + case MSG_UPDATE_NOTIFICATION_BITMAP: + if (player != null && isNotificationStarted && currentNotificationTag == msg.arg1) { + startOrUpdateNotification(player, (Bitmap) msg.obj); + } + break; + default: + return false; + } + return true; + } + private static Map createPlaybackActions( Context context, int instanceId) { Map actions = new HashMap<>(); @@ -1326,37 +1349,37 @@ public class PlayerNotificationManager { @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onIsPlayingChanged(boolean isPlaying) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onTimelineChanged(Timeline timeline, int reason) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onPositionDiscontinuity(int reason) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - startOrUpdateNotification(); + postStartOrUpdateNotification(); } } From 6a354bb29fc4c0cc8a13888fb6de2de721da3ba4 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 5 Dec 2019 10:19:27 +0000 Subject: [PATCH 786/807] Merge pull request #6595 from szaboa:dev-v2-ssa-position PiperOrigin-RevId: 283722376 --- RELEASENOTES.md | 6 + constants.gradle | 1 + library/core/build.gradle | 2 + .../exoplayer2/text/ssa/SsaDecoder.java | 387 ++++++++++++++---- .../text/ssa/SsaDialogueFormat.java | 83 ++++ .../android/exoplayer2/text/ssa/SsaStyle.java | 284 +++++++++++++ .../exoplayer2/text/ssa/SsaSubtitle.java | 21 +- .../src/test/assets/ssa/invalid_positioning | 16 + .../src/test/assets/ssa/overlapping_timecodes | 12 + library/core/src/test/assets/ssa/positioning | 18 + .../assets/ssa/positioning_without_playres | 7 + library/core/src/test/assets/ssa/typical | 6 +- .../exoplayer2/text/ssa/SsaDecoderTest.java | 176 ++++++++ 13 files changed, 920 insertions(+), 99 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java create mode 100644 library/core/src/test/assets/ssa/invalid_positioning create mode 100644 library/core/src/test/assets/ssa/overlapping_timecodes create mode 100644 library/core/src/test/assets/ssa/positioning create mode 100644 library/core/src/test/assets/ssa/positioning_without_playres diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0574445600..b1b39b75b1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -83,6 +83,12 @@ ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * Allow `AdtsExtractor` to encounter EoF when calculating average frame size ([#6700](https://github.com/google/ExoPlayer/issues/6700)). +* Text: + * Require an end time or duration for SubRip (SRT) and SubStation Alpha + (SSA/ASS) subtitles. This applies to both sidecar files & subtitles + [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). + * Reconfigure audio sink when PCM encoding changes + ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. diff --git a/constants.gradle b/constants.gradle index 65812e4274..599af54dde 100644 --- a/constants.gradle +++ b/constants.gradle @@ -20,6 +20,7 @@ project.ext { targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved compileSdkVersion = 29 dexmakerVersion = '2.21.0' + guavaVersion = '23.5-android' mockitoVersion = '2.25.0' robolectricVersion = '4.3' autoValueVersion = '1.6' diff --git a/library/core/build.gradle b/library/core/build.gradle index e145a179d9..3cc14326c5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -53,6 +53,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion + androidTestImplementation 'com.google.guava:guava:' + guavaVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion @@ -60,6 +61,7 @@ dependencies { testImplementation 'androidx.test:core:' + androidxTestCoreVersion testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation 'com.google.truth:truth:' + truthVersion + testImplementation 'com.google.guava:guava:' + guavaVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils') 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 2e78b433bd..d751772879 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,7 +15,7 @@ */ package com.google.android.exoplayer2.text.ssa; -import android.text.TextUtils; +import android.text.Layout; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -23,71 +23,90 @@ 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; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * A {@link SimpleSubtitleDecoder} for SSA/ASS. - */ +/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */ public final class SsaDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SsaDecoder"; private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)"); - private static final String FORMAT_LINE_PREFIX = "Format: "; - private static final String DIALOGUE_LINE_PREFIX = "Dialogue: "; + + /* package */ static final String FORMAT_LINE_PREFIX = "Format:"; + /* package */ static final String STYLE_LINE_PREFIX = "Style:"; + private static final String DIALOGUE_LINE_PREFIX = "Dialogue:"; + + private static final float DEFAULT_MARGIN = 0.05f; private final boolean haveInitializationData; + @Nullable private final SsaDialogueFormat dialogueFormatFromInitializationData; - private int formatKeyCount; - private int formatStartIndex; - private int formatEndIndex; - private int formatTextIndex; + private @MonotonicNonNull Map styles; + + /** + * The horizontal resolution used by the subtitle author - all cue positions are relative to this. + * + *

        Parsed from the {@code PlayResX} value in the {@code [Script Info]} section. + */ + private float screenWidth; + /** + * The vertical resolution used by the subtitle author - all cue positions are relative to this. + * + *

        Parsed from the {@code PlayResY} value in the {@code [Script Info]} section. + */ + private float screenHeight; public SsaDecoder() { this(/* initializationData= */ null); } /** + * Constructs an SsaDecoder with optional format & header info. + * * @param initializationData Optional initialization data for the decoder. If not null or empty, * the initialization data must consist of two byte arrays. The first must contain an SSA * format line. The second must contain an SSA header that will be assumed common to all - * samples. + * samples. The header is everything in an SSA file before the {@code [Events]} section (i.e. + * {@code [Script Info]} and optional {@code [V4+ Styles]} section. */ public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); + screenWidth = Cue.DIMEN_UNSET; + screenHeight = Cue.DIMEN_UNSET; + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); - parseFormatLine(formatLine); + dialogueFormatFromInitializationData = + Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine)); parseHeader(new ParsableByteArray(initializationData.get(1))); } else { haveInitializationData = false; + dialogueFormatFromInitializationData = null; } } @Override protected Subtitle decode(byte[] bytes, int length, boolean reset) { - ArrayList cues = new ArrayList<>(); - LongArray cueTimesUs = new LongArray(); + List> cues = new ArrayList<>(); + List cueTimesUs = new ArrayList<>(); ParsableByteArray data = new ParsableByteArray(bytes, length); if (!haveInitializationData) { parseHeader(data); } parseEventBody(data, cues, cueTimesUs); - - Cue[] cuesArray = new Cue[cues.size()]; - cues.toArray(cuesArray); - long[] cueTimesUsArray = cueTimesUs.toArray(); - return new SsaSubtitle(cuesArray, cueTimesUsArray); + return new SsaSubtitle(cues, cueTimesUs); } /** @@ -98,109 +117,157 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private void parseHeader(ParsableByteArray data) { String currentLine; while ((currentLine = data.readLine()) != null) { - // TODO: Parse useful data from the header. - if (currentLine.startsWith("[Events]")) { - // We've reached the event body. + if ("[Script Info]".equalsIgnoreCase(currentLine)) { + parseScriptInfo(data); + } else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) { + styles = parseStyles(data); + } else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) { + Log.i(TAG, "[V4 Styles] are not supported"); + } else if ("[Events]".equalsIgnoreCase(currentLine)) { + // We've reached the [Events] section, so the header is over. return; } } } + /** + * Parse the {@code [Script Info]} section. + * + *

        When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position} + * set to the beginning of of the first line after {@code [Script Info]}. + */ + private void parseScriptInfo(ParsableByteArray data) { + String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + String[] infoNameAndValue = currentLine.split(":"); + if (infoNameAndValue.length != 2) { + continue; + } + switch (Util.toLowerInvariant(infoNameAndValue[0].trim())) { + case "playresx": + try { + screenWidth = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResX value. + } + break; + case "playresy": + try { + screenHeight = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResY value. + } + break; + } + } + } + + /** + * Parse the {@code [V4+ Styles]} section. + * + *

        When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing + * at the beginning of of the first line after {@code [V4+ Styles]}. + */ + private static Map parseStyles(ParsableByteArray data) { + SsaStyle.Format formatInfo = null; + Map styles = new LinkedHashMap<>(); + String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + formatInfo = SsaStyle.Format.fromFormatLine(currentLine); + } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) { + if (formatInfo == null) { + Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); + continue; + } + SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + if (style != null) { + styles.put(style.name, style); + } + } + } + return styles; + } + /** * Parses the event body of the subtitle. * * @param data A {@link ParsableByteArray} from which the body should be read. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; String currentLine; while ((currentLine = data.readLine()) != null) { - if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { - parseFormatLine(currentLine); + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + format = SsaDialogueFormat.fromFormatLine(currentLine); } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { - parseDialogueLine(currentLine, cues, cueTimesUs); + if (format == null) { + Log.w(TAG, "Skipping dialogue line before complete format: " + currentLine); + continue; + } + parseDialogueLine(currentLine, format, cues, cueTimesUs); } } } - /** - * Parses a format line. - * - * @param formatLine The line to parse. - */ - private void parseFormatLine(String formatLine) { - String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); - formatKeyCount = values.length; - formatStartIndex = C.INDEX_UNSET; - formatEndIndex = C.INDEX_UNSET; - formatTextIndex = C.INDEX_UNSET; - for (int i = 0; i < formatKeyCount; i++) { - String key = Util.toLowerInvariant(values[i].trim()); - switch (key) { - case "start": - formatStartIndex = i; - break; - case "end": - formatEndIndex = i; - break; - case "text": - formatTextIndex = i; - break; - default: - // Do nothing. - break; - } - } - if (formatStartIndex == C.INDEX_UNSET - || formatEndIndex == C.INDEX_UNSET - || formatTextIndex == C.INDEX_UNSET) { - // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. - formatKeyCount = 0; - } - } - /** * Parses a dialogue line. * - * @param dialogueLine The line to parse. + * @param dialogueLine The dialogue values (i.e. everything after {@code Dialogue:}). + * @param format The dialogue format to use when parsing {@code dialogueLine}. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { - if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); - return; - } - - String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) - .split(",", formatKeyCount); - if (lineValues.length != formatKeyCount) { + private void parseDialogueLine( + String dialogueLine, SsaDialogueFormat format, List> cues, List cueTimesUs) { + Assertions.checkArgument(dialogueLine.startsWith(DIALOGUE_LINE_PREFIX)); + String[] lineValues = + dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()).split(",", format.length); + if (lineValues.length != format.length) { Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); return; } - long startTimeUs = parseTimecodeUs(lineValues[formatStartIndex]); + long startTimeUs = parseTimecodeUs(lineValues[format.startTimeIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } - long endTimeUs = parseTimecodeUs(lineValues[formatEndIndex]); + long endTimeUs = parseTimecodeUs(lineValues[format.endTimeIndex]); if (endTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } + SsaStyle style = + styles != null && format.styleIndex != C.INDEX_UNSET + ? styles.get(lineValues[format.styleIndex].trim()) + : null; + String rawText = lineValues[format.textIndex]; + SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText); String text = - lineValues[formatTextIndex] - .replaceAll("\\{.*?\\}", "") // Warning that \\} can be replaced with } is bogus. + SsaStyle.Overrides.stripStyleOverrides(rawText) .replaceAll("\\\\N", "\n") .replaceAll("\\\\n", "\n"); - cues.add(new Cue(text)); - cueTimesUs.add(startTimeUs); - cues.add(Cue.EMPTY); - cueTimesUs.add(endTimeUs); + Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight); + + int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues); + int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues); + // Iterate on cues from startTimeIndex until endTimeIndex, adding the current cue. + for (int i = startTimeIndex; i < endTimeIndex; i++) { + cues.get(i).add(cue); + } } /** @@ -209,8 +276,8 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param timeString The string to parse. * @return The parsed timestamp in microseconds. */ - public static long parseTimecodeUs(String timeString) { - Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString); + private static long parseTimecodeUs(String timeString) { + Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim()); if (!matcher.matches()) { return C.TIME_UNSET; } @@ -221,4 +288,154 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return timestampUs; } + private static Cue createCue( + String text, + @Nullable SsaStyle style, + SsaStyle.Overrides styleOverrides, + float screenWidth, + float screenHeight) { + @SsaStyle.SsaAlignment int alignment; + if (styleOverrides.alignment != SsaStyle.SsaAlignment.UNKNOWN) { + alignment = styleOverrides.alignment; + } else if (style != null) { + alignment = style.alignment; + } else { + alignment = SsaStyle.SsaAlignment.UNKNOWN; + } + @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment); + @Cue.AnchorType int lineAnchor = toLineAnchor(alignment); + + float position; + float line; + if (styleOverrides.position != null + && screenHeight != Cue.DIMEN_UNSET + && screenWidth != Cue.DIMEN_UNSET) { + position = styleOverrides.position.x / screenWidth; + line = styleOverrides.position.y / screenHeight; + } else { + // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines. + position = computeDefaultLineOrPosition(positionAnchor); + line = computeDefaultLineOrPosition(lineAnchor); + } + + return new Cue( + text, + toTextAlignment(alignment), + line, + Cue.LINE_TYPE_FRACTION, + lineAnchor, + position, + positionAnchor, + /* size= */ Cue.DIMEN_UNSET); + } + + @Nullable + private static Layout.Alignment toTextAlignment(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SsaAlignment.BOTTOM_LEFT: + case SsaStyle.SsaAlignment.MIDDLE_LEFT: + case SsaStyle.SsaAlignment.TOP_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case SsaStyle.SsaAlignment.BOTTOM_CENTER: + case SsaStyle.SsaAlignment.MIDDLE_CENTER: + case SsaStyle.SsaAlignment.TOP_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case SsaStyle.SsaAlignment.BOTTOM_RIGHT: + case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + case SsaStyle.SsaAlignment.TOP_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + case SsaStyle.SsaAlignment.UNKNOWN: + return null; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return null; + } + } + + @Cue.AnchorType + private static int toLineAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SsaAlignment.BOTTOM_LEFT: + case SsaStyle.SsaAlignment.BOTTOM_CENTER: + case SsaStyle.SsaAlignment.BOTTOM_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SsaAlignment.MIDDLE_LEFT: + case SsaStyle.SsaAlignment.MIDDLE_CENTER: + case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SsaAlignment.TOP_LEFT: + case SsaStyle.SsaAlignment.TOP_CENTER: + case SsaStyle.SsaAlignment.TOP_RIGHT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SsaAlignment.UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + @Cue.AnchorType + private static int toPositionAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SsaAlignment.BOTTOM_LEFT: + case SsaStyle.SsaAlignment.MIDDLE_LEFT: + case SsaStyle.SsaAlignment.TOP_LEFT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SsaAlignment.BOTTOM_CENTER: + case SsaStyle.SsaAlignment.MIDDLE_CENTER: + case SsaStyle.SsaAlignment.TOP_CENTER: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SsaAlignment.BOTTOM_RIGHT: + case SsaStyle.SsaAlignment.MIDDLE_RIGHT: + case SsaStyle.SsaAlignment.TOP_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SsaAlignment.UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + private static float computeDefaultLineOrPosition(@Cue.AnchorType int anchor) { + switch (anchor) { + case Cue.ANCHOR_TYPE_START: + return DEFAULT_MARGIN; + case Cue.ANCHOR_TYPE_MIDDLE: + return 0.5f; + case Cue.ANCHOR_TYPE_END: + return 1.0f - DEFAULT_MARGIN; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; + } + } + + /** + * Searches for {@code timeUs} in {@code sortedCueTimesUs}, inserting it if it's not found, and + * returns the index. + * + *

        If it's inserted, we also insert a matching entry to {@code cues}. + */ + private static int addCuePlacerholderByTime( + long timeUs, List sortedCueTimesUs, List> cues) { + int insertionIndex = 0; + for (int i = sortedCueTimesUs.size() - 1; i >= 0; i--) { + if (sortedCueTimesUs.get(i) == timeUs) { + return i; + } + + if (sortedCueTimesUs.get(i) < timeUs) { + insertionIndex = i + 1; + break; + } + } + sortedCueTimesUs.add(insertionIndex, timeUs); + // Copy over cues from left, or use an empty list if we're inserting at the beginning. + cues.add( + insertionIndex, + insertionIndex == 0 ? new ArrayList<>() : new ArrayList<>(cues.get(insertionIndex - 1))); + return insertionIndex; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java new file mode 100644 index 0000000000..03c025cd94 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java @@ -0,0 +1,83 @@ +/* + * 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.text.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX; + +import android.text.TextUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * Represents a {@code Format:} line from the {@code [Events]} section + * + *

        The indices are used to determine the location of particular properties in each {@code + * Dialogue:} line. + */ +/* package */ final class SsaDialogueFormat { + + public final int startTimeIndex; + public final int endTimeIndex; + public final int styleIndex; + public final int textIndex; + public final int length; + + private SsaDialogueFormat( + int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) { + this.startTimeIndex = startTimeIndex; + this.endTimeIndex = endTimeIndex; + this.styleIndex = styleIndex; + this.textIndex = textIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [Events] section. + * + * @return the parsed info, or null if {@code formatLine} doesn't contain both 'start' and 'end'. + */ + @Nullable + public static SsaDialogueFormat fromFormatLine(String formatLine) { + int startTimeIndex = C.INDEX_UNSET; + int endTimeIndex = C.INDEX_UNSET; + int styleIndex = C.INDEX_UNSET; + int textIndex = C.INDEX_UNSET; + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "start": + startTimeIndex = i; + break; + case "end": + endTimeIndex = i; + break; + case "style": + styleIndex = i; + break; + case "text": + textIndex = i; + break; + } + } + return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) + ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) + : null; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java new file mode 100644 index 0000000000..e8070976e7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -0,0 +1,284 @@ +/* + * 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.text.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.graphics.PointF; +import android.text.TextUtils; +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.Log; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Represents a line from an SSA/ASS {@code [V4+ Styles]} section. */ +/* package */ final class SsaStyle { + + private static final String TAG = "SsaStyle"; + + public final String name; + @SsaAlignment public final int alignment; + + private SsaStyle(String name, @SsaAlignment int alignment) { + this.name = name; + this.alignment = alignment; + } + + @Nullable + public static SsaStyle fromStyleLine(String styleLine, Format format) { + Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX)); + String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ","); + if (styleValues.length != format.length) { + Log.w( + TAG, + Util.formatInvariant( + "Skipping malformed 'Style:' line (expected %s values, found %s): '%s'", + format.length, styleValues.length, styleLine)); + return null; + } + try { + return new SsaStyle( + styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); + } catch (RuntimeException e) { + Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); + return null; + } + } + + @SsaAlignment + private static int parseAlignment(String alignmentStr) { + try { + @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); + if (isValidAlignment(alignment)) { + return alignment; + } + } catch (NumberFormatException e) { + // Swallow the exception and return UNKNOWN below. + } + Log.w(TAG, "Ignoring unknown alignment: " + alignmentStr); + return SsaAlignment.UNKNOWN; + } + + private static boolean isValidAlignment(@SsaAlignment int alignment) { + switch (alignment) { + case SsaAlignment.BOTTOM_CENTER: + case SsaAlignment.BOTTOM_LEFT: + case SsaAlignment.BOTTOM_RIGHT: + case SsaAlignment.MIDDLE_CENTER: + case SsaAlignment.MIDDLE_LEFT: + case SsaAlignment.MIDDLE_RIGHT: + case SsaAlignment.TOP_CENTER: + case SsaAlignment.TOP_LEFT: + case SsaAlignment.TOP_RIGHT: + return true; + case SsaAlignment.UNKNOWN: + default: + return false; + } + } + + /** + * Represents a {@code Format:} line from the {@code [V4+ Styles]} section + * + *

        The indices are used to determine the location of particular properties in each {@code + * Style:} line. + */ + /* package */ static final class Format { + + public final int nameIndex; + public final int alignmentIndex; + public final int length; + + private Format(int nameIndex, int alignmentIndex, int length) { + this.nameIndex = nameIndex; + this.alignmentIndex = alignmentIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [V4+ Styles] section. + * + * @return the parsed info, or null if {@code styleFormatLine} doesn't contain 'name'. + */ + @Nullable + public static Format fromFormatLine(String styleFormatLine) { + int nameIndex = C.INDEX_UNSET; + int alignmentIndex = C.INDEX_UNSET; + String[] keys = + TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "name": + nameIndex = i; + break; + case "alignment": + alignmentIndex = i; + break; + } + } + return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null; + } + } + + /** + * Represents the style override information parsed from an SSA/ASS dialogue line. + * + *

        Overrides are contained in braces embedded in the dialogue text of the cue. + */ + /* package */ static final class Overrides { + + private static final String TAG = "SsaStyle.Overrides"; + + /** Matches "{foo}" and returns "foo" in group 1 */ + // Warning that \\} can be replaced with } is bogus [internal: b/144480183]. + private static final Pattern BRACES_PATTERN = Pattern.compile("\\{([^}]*)\\}"); + + private static final String PADDED_DECIMAL_PATTERN = "\\s*\\d+(?:\\.\\d+)?\\s*"; + + /** Matches "\pos(x,y)" and returns "x" in group 1 and "y" in group 2 */ + private static final Pattern POSITION_PATTERN = + Pattern.compile(Util.formatInvariant("\\\\pos\\((%1$s),(%1$s)\\)", PADDED_DECIMAL_PATTERN)); + /** Matches "\move(x1,y1,x2,y2[,t1,t2])" and returns "x2" in group 1 and "y2" in group 2 */ + private static final Pattern MOVE_PATTERN = + Pattern.compile( + Util.formatInvariant( + "\\\\move\\(%1$s,%1$s,(%1$s),(%1$s)(?:,%1$s,%1$s)?\\)", PADDED_DECIMAL_PATTERN)); + + /** Matches "\anx" and returns x in group 1 */ + private static final Pattern ALIGNMENT_OVERRIDE_PATTERN = Pattern.compile("\\\\an(\\d+)"); + + @SsaAlignment public final int alignment; + @Nullable public final PointF position; + + private Overrides(@SsaAlignment int alignment, @Nullable PointF position) { + this.alignment = alignment; + this.position = position; + } + + public static Overrides parseFromDialogue(String text) { + @SsaAlignment int alignment = SsaAlignment.UNKNOWN; + PointF position = null; + Matcher matcher = BRACES_PATTERN.matcher(text); + while (matcher.find()) { + String braceContents = matcher.group(1); + try { + PointF parsedPosition = parsePosition(braceContents); + if (parsedPosition != null) { + position = parsedPosition; + } + } catch (RuntimeException e) { + // Ignore invalid \pos() or \move() function. + } + try { + @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents); + if (parsedAlignment != SsaAlignment.UNKNOWN) { + alignment = parsedAlignment; + } + } catch (RuntimeException e) { + // Ignore invalid \an alignment override. + } + } + return new Overrides(alignment, position); + } + + public static String stripStyleOverrides(String dialogueLine) { + return BRACES_PATTERN.matcher(dialogueLine).replaceAll(""); + } + + /** + * Parses the position from a style override, returns null if no position is found. + * + *

        The attribute is expected to be in the form {@code \pos(x,y)} or {@code + * \move(x1,y1,x2,y2,startTime,endTime)} (startTime and endTime are optional). In the case of + * {@code \move()}, this returns {@code (x2, y2)} (i.e. the end position of the move). + * + * @param styleOverride The string to parse. + * @return The parsed position, or null if no position is found. + */ + @Nullable + private static PointF parsePosition(String styleOverride) { + Matcher positionMatcher = POSITION_PATTERN.matcher(styleOverride); + Matcher moveMatcher = MOVE_PATTERN.matcher(styleOverride); + boolean hasPosition = positionMatcher.find(); + boolean hasMove = moveMatcher.find(); + + String x; + String y; + if (hasPosition) { + if (hasMove) { + Log.i( + TAG, + "Override has both \\pos(x,y) and \\move(x1,y1,x2,y2); using \\pos values. override='" + + styleOverride + + "'"); + } + x = positionMatcher.group(1); + y = positionMatcher.group(2); + } else if (hasMove) { + x = moveMatcher.group(1); + y = moveMatcher.group(2); + } else { + return null; + } + return new PointF( + Float.parseFloat(Assertions.checkNotNull(x).trim()), + Float.parseFloat(Assertions.checkNotNull(y).trim())); + } + + @SsaAlignment + private static int parseAlignmentOverride(String braceContents) { + Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents); + return matcher.find() ? parseAlignment(matcher.group(1)) : SsaAlignment.UNKNOWN; + } + } + + /** The SSA/ASS alignments. */ + @IntDef({ + SsaAlignment.UNKNOWN, + SsaAlignment.BOTTOM_LEFT, + SsaAlignment.BOTTOM_CENTER, + SsaAlignment.BOTTOM_RIGHT, + SsaAlignment.MIDDLE_LEFT, + SsaAlignment.MIDDLE_CENTER, + SsaAlignment.MIDDLE_RIGHT, + SsaAlignment.TOP_LEFT, + SsaAlignment.TOP_CENTER, + SsaAlignment.TOP_RIGHT, + }) + @Documented + @Retention(SOURCE) + /* package */ @interface SsaAlignment { + // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad). + int UNKNOWN = -1; + int BOTTOM_LEFT = 1; + int BOTTOM_CENTER = 2; + int BOTTOM_RIGHT = 3; + int MIDDLE_LEFT = 4; + int MIDDLE_CENTER = 5; + int MIDDLE_RIGHT = 6; + int TOP_LEFT = 7; + int TOP_CENTER = 8; + int TOP_RIGHT = 9; + } +} 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 9a3756194f..4093f7974d 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 @@ -28,14 +28,14 @@ import java.util.List; */ /* package */ final class SsaSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List> cues; + private final List cueTimesUs; /** * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ - public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { + public SsaSubtitle(List> cues, List cueTimesUs) { this.cues = cues; this.cueTimesUs = cueTimesUs; } @@ -43,30 +43,29 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : C.INDEX_UNSET; + return index < cueTimesUs.size() ? index : C.INDEX_UNSET; } @Override public int getEventTimeCount() { - return cueTimesUs.length; + return cueTimesUs.size(); } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + Assertions.checkArgument(index < cueTimesUs.size()); + return cueTimesUs.get(index); } @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == Cue.EMPTY) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. + if (index == -1) { + // timeUs is earlier than the start of the first cue. return Collections.emptyList(); } else { - return Collections.singletonList(cues[index]); + return cues.get(index); } } - } diff --git a/library/core/src/test/assets/ssa/invalid_positioning b/library/core/src/test/assets/ssa/invalid_positioning new file mode 100644 index 0000000000..ade4cce9c4 --- /dev/null +++ b/library/core/src/test/assets/ssa/invalid_positioning @@ -0,0 +1,16 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 +PlayResY: 200 + +[V4+ Styles] +! Alignment is set to 4 - i.e. middle-left +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(-5,50)}First subtitle (negative \pos()). +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,{\move(-5,50,-5,50)}Second subtitle (negative \move()). +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,{\an11}Third subtitle (invalid alignment). +Dialogue: 0,0:00:09:56,0:00:12:90,Default,Olly,\pos(150,100) Fourth subtitle (no braces). diff --git a/library/core/src/test/assets/ssa/overlapping_timecodes b/library/core/src/test/assets/ssa/overlapping_timecodes new file mode 100644 index 0000000000..2093a96ac5 --- /dev/null +++ b/library/core/src/test/assets/ssa/overlapping_timecodes @@ -0,0 +1,12 @@ +[Script Info] +Title: SomeTitle + +[Events] +Format: Start, End, Text +Dialogue: 0:00:01.00,0:00:04.23,First subtitle - end overlaps second +Dialogue: 0:00:02.00,0:00:05.23,Second subtitle - beginning overlaps first +Dialogue: 0:00:08.44,0:00:09.44,Fourth subtitle - same timings as fifth +Dialogue: 0:00:06.00,0:00:08.44,Third subtitle - out of order +Dialogue: 0:00:08.44,0:00:09.44,Fifth subtitle - same timings as fourth +Dialogue: 0:00:10.72,0:00:15.65,Sixth subtitle - fully encompasses seventh +Dialogue: 0:00:13.22,0:00:14.22,Seventh subtitle - nested fully inside sixth diff --git a/library/core/src/test/assets/ssa/positioning b/library/core/src/test/assets/ssa/positioning new file mode 100644 index 0000000000..af19fc3724 --- /dev/null +++ b/library/core/src/test/assets/ssa/positioning @@ -0,0 +1,18 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 +PlayResY: 202 + +[V4+ Styles] +! Alignment is set to 4 - i.e. middle-left +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,4,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(150,50.5)}First subtitle. +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,Second subtitle{\pos(75,50.5)}. +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,{\pos(150,100)}Third subtitle{\pos(75,101)}, (only last counts). +Dialogue: 0,0:00:09:56,0:00:12:90,Default,Olly,{\move(150,100,150,50.5)}Fourth subtitle. +Dialogue: 0,0:00:13:56,0:00:15:90,Default,Olly,{ \pos( 150, 101 ) }Fifth subtitle {\an2}(alignment override, spaces around pos arguments). +Dialogue: 0,0:00:16:56,0:00:19:90,Default,Olly,{\pos(150,101)\an9}Sixth subtitle (multiple overrides in same braces). diff --git a/library/core/src/test/assets/ssa/positioning_without_playres b/library/core/src/test/assets/ssa/positioning_without_playres new file mode 100644 index 0000000000..75b7967b34 --- /dev/null +++ b/library/core/src/test/assets/ssa/positioning_without_playres @@ -0,0 +1,7 @@ +[Script Info] +Title: SomeTitle +PlayResX: 300 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,{\pos(150,50)}First subtitle. diff --git a/library/core/src/test/assets/ssa/typical b/library/core/src/test/assets/ssa/typical index 4542af1217..3d36503251 100644 --- a/library/core/src/test/assets/ssa/typical +++ b/library/core/src/test/assets/ssa/typical @@ -7,6 +7,6 @@ Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000 [Events] Format: Layer, Start, End, Style, Name, Text -Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. -Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another. -Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. +Dialogue: 0,0:00:00.00,0:00:01.23,Default ,Olly,This is the first subtitle{ignored}. +Dialogue: 0,0:00:02.34,0:00:03.45,Default ,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04:56,0:00:08:90,Default ,Olly,This is the third subtitle, with a comma. 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 3c48aa61dd..9112bec398 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 @@ -16,11 +16,15 @@ package com.google.android.exoplayer2.text.ssa; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import android.text.Layout; 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 com.google.common.collect.Iterables; import java.io.IOException; import java.util.ArrayList; import org.junit.Test; @@ -35,7 +39,11 @@ public final class SsaDecoderTest { private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header"; private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue"; private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format"; + private static final String OVERLAPPING_TIMECODES = "ssa/overlapping_timecodes"; + private static final String POSITIONS = "ssa/positioning"; private static final String INVALID_TIMECODES = "ssa/invalid_timecodes"; + private static final String INVALID_POSITIONS = "ssa/invalid_positioning"; + private static final String POSITIONS_WITHOUT_PLAYRES = "ssa/positioning_without_playres"; @Test public void testDecodeEmpty() throws IOException { @@ -54,6 +62,19 @@ public final class SsaDecoderTest { Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center). + Cue firstCue = subtitle.getCues(subtitle.getEventTime(0)).get(0); + assertWithMessage("Cue.textAlignment") + .that(firstCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertWithMessage("Cue.positionAnchor") + .that(firstCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.lineAnchor").that(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.95f); + assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); assertTypicalCue3(subtitle, 4); @@ -79,6 +100,161 @@ public final class SsaDecoderTest { assertTypicalCue3(subtitle, 4); } + @Test + public void testDecodeOverlappingTimecodes() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000); + assertThat(subtitle.getEventTime(2)).isEqualTo(4_230_000); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_230_000); + assertThat(subtitle.getEventTime(4)).isEqualTo(6_000_000); + assertThat(subtitle.getEventTime(5)).isEqualTo(8_440_000); + assertThat(subtitle.getEventTime(6)).isEqualTo(9_440_000); + assertThat(subtitle.getEventTime(7)).isEqualTo(10_720_000); + assertThat(subtitle.getEventTime(8)).isEqualTo(13_220_000); + assertThat(subtitle.getEventTime(9)).isEqualTo(14_220_000); + assertThat(subtitle.getEventTime(10)).isEqualTo(15_650_000); + + String firstSubtitleText = "First subtitle - end overlaps second"; + String secondSubtitleText = "Second subtitle - beginning overlaps first"; + String thirdSubtitleText = "Third subtitle - out of order"; + String fourthSubtitleText = "Fourth subtitle - same timings as fifth"; + String fifthSubtitleText = "Fifth subtitle - same timings as fourth"; + String sixthSubtitleText = "Sixth subtitle - fully encompasses seventh"; + String seventhSubtitleText = "Seventh subtitle - nested fully inside sixth"; + assertThat(Iterables.transform(subtitle.getCues(1_000_010), cue -> cue.text.toString())) + .containsExactly(firstSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(2_000_010), cue -> cue.text.toString())) + .containsExactly(firstSubtitleText, secondSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(4_230_010), cue -> cue.text.toString())) + .containsExactly(secondSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(5_230_010), cue -> cue.text.toString())) + .isEmpty(); + assertThat(Iterables.transform(subtitle.getCues(6_000_010), cue -> cue.text.toString())) + .containsExactly(thirdSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(8_440_010), cue -> cue.text.toString())) + .containsExactly(fourthSubtitleText, fifthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(9_440_010), cue -> cue.text.toString())) + .isEmpty(); + assertThat(Iterables.transform(subtitle.getCues(10_720_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(13_220_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText, seventhSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(14_220_010), cue -> cue.text.toString())) + .containsExactly(sixthSubtitleText); + assertThat(Iterables.transform(subtitle.getCues(15_650_010), cue -> cue.text.toString())) + .isEmpty(); + } + + @Test + public void testDecodePositions() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // Check \pos() sets position & line + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.25f); + + // Check the \pos() doesn't need to be at the start of the line. + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.25f); + assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.25f); + + // Check only the last \pos() value is used. + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertWithMessage("Cue.position").that(thirdCue.position).isEqualTo(0.25f); + + // Check \move() is treated as \pos() + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.25f); + + // Check alignment override in a separate brace (to bottom-center) affects textAlignment and + // both line & position anchors. + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertWithMessage("Cue.position").that(fifthCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.line").that(fifthCue.line).isEqualTo(0.5f); + assertWithMessage("Cue.positionAnchor") + .that(fifthCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.lineAnchor").that(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.textAlignment") + .that(fifthCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + + // Check alignment override in the same brace (to top-right) affects textAlignment and both line + // & position anchors. + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertWithMessage("Cue.position").that(sixthCue.position).isEqualTo(0.5f); + assertWithMessage("Cue.line").that(sixthCue.line).isEqualTo(0.5f); + assertWithMessage("Cue.positionAnchor") + .that(sixthCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + assertWithMessage("Cue.lineAnchor").that(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertWithMessage("Cue.textAlignment") + .that(sixthCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + } + + @Test + public void testDecodeInvalidPositions() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // Negative parameter to \pos() - fall back to the positions implied by middle-left alignment. + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.05f); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.5f); + + // Negative parameter to \move() - fall back to the positions implied by middle-left alignment. + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.05f); + assertWithMessage("Cue.lineType").that(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.5f); + + // Check invalid alignment override (11) is skipped and style-provided one is used (4). + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertWithMessage("Cue.positionAnchor") + .that(thirdCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + assertWithMessage("Cue.lineAnchor").that(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertWithMessage("Cue.textAlignment") + .that(thirdCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_NORMAL); + + // No braces - fall back to the positions implied by middle-left alignment + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.05f); + assertWithMessage("Cue.lineType").that(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.5f); + } + + @Test + public void testDecodePositionsWithMissingPlayResY() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + // The dialogue line has a valid \pos() override, but it's ignored because PlayResY isn't + // set (so we don't know the denominator). + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); + assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + } + @Test public void testDecodeInvalidTimecodes() throws IOException { // Parsing should succeed, parsing the third cue only. From 86a86f6466987f97954e16a0bc0b6d256fa53944 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 4 Dec 2019 11:41:38 +0000 Subject: [PATCH 787/807] Refactor ExtractorInput javadoc about allowEndOfInput This parameter is a little confusing, especially as the behaviour can be surprising if the intended use-case isn't clear. This change moves the description of the parameter into the class javadoc, adds context/justification and slims down each method's javadoc to refer to the class-level. Related to investigating/fixing issue:#6700 PiperOrigin-RevId: 283724826 --- .../exoplayer2/extractor/ExtractorInput.java | 94 +++++++++++++------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java index 45650c45fa..1b492e38c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -18,9 +18,50 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; /** * Provides data to be consumed by an {@link Extractor}. + * + *

        This interface provides two modes of accessing the underlying input. See the subheadings below + * for more info about each mode. + * + *

          + *
        • The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level + * access operations. + *
        • The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + *
        + * + *

        {@link InputStream}-like methods

        + * + *

        The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level + * access operations. The {@code length} parameter is a maximum, and each method returns the number + * of bytes actually processed. This may be less than {@code length} because the end of the input + * was reached, or the method was interrupted, or the operation was aborted early for another + * reason. + * + *

        Block-based methods

        + * + *

        The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * + *

        These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This + * parameter is intended to be set to true when the caller believes the input might be fully + * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final + * block/frame/header). It's not intended to allow a partial read (i.e. greater than 0 bytes, + * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from + * these methods (a partial read is assumed to indicate a malformed block/frame/header - and + * therefore a malformed file). + * + *

        The expected behaviour of the block-based methods is therefore: + * + *

          + *
        • Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}. + *
        • Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}. + *
        • Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException} + * (regardless of {@code allowEndOfInput}). + *
        */ public interface ExtractorInput { @@ -41,22 +82,16 @@ public interface ExtractorInput { /** * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full. - *

        - * If the end of the input is found having read no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

        - * Encountering the end of input having partially satisfied the read is always considered an - * error, and will result in an {@link EOFException} being thrown. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. * @param length The number of bytes to read from the input. * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the read was successful. False if the end of the input was encountered having - * read no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having read no data. * @throws EOFException If the end of input was encountered having partially satisfied the read * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were * read and {@code allowEndOfInput} is false. @@ -94,9 +129,10 @@ public interface ExtractorInput { * @param length The number of bytes to skip from the input. * @param allowEndOfInput True if encountering the end of the input having skipped no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the skip was successful. False if the end of the input was encountered having - * skipped no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having skipped no data. * @throws EOFException If the end of input was encountered having partially satisfied the skip * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were * skipped and {@code allowEndOfInput} is false. @@ -121,12 +157,8 @@ public interface ExtractorInput { /** * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index * {@code offset}. The current read position is left unchanged. - *

        - * If the end of the input is found having peeked no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

        - * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * + *

        Calling {@link #resetPeekPosition()} resets the peek position to equal the current read * position, so the caller can peek the same data again. Reading or skipping also resets the peek * position. * @@ -135,9 +167,10 @@ public interface ExtractorInput { * @param length The number of bytes to peek from the input. * @param allowEndOfInput True if encountering the end of the input having peeked no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the peek was successful. False if the end of the input was encountered having - * peeked no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having peeked no data. * @throws EOFException If the end of input was encountered having partially satisfied the peek * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were * peeked and {@code allowEndOfInput} is false. @@ -165,18 +198,16 @@ public interface ExtractorInput { void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. - *

        - * If the end of the input is encountered before advancing the peek position, then behavior is - * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is - * returned. Otherwise an {@link EOFException} is thrown. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int, + * boolean)} except the data is skipped instead of read. * * @param length The number of bytes by which to advance the peek position. * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, * and should result in {@code false} being returned. False if it should be considered an - * error, causing an {@link EOFException} to be thrown. - * @return True if advancing the peek position was successful. False if the end of the input was - * encountered before the peek position could be advanced. + * error, causing an {@link EOFException} to be thrown. See note in class Javadoc. + * @return True if advancing the peek position was successful. False if {@code + * allowEndOfInput=true} and the end of the input was encountered before advancing over any + * data. * @throws EOFException If the end of input was encountered having partially advanced (i.e. having * advanced by at least one byte, but fewer than {@code length}), or if the end of input was * encountered before advancing and {@code allowEndOfInput} is false. @@ -187,7 +218,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int,)} + * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. * @throws EOFException If the end of input was encountered. From 7d7c37b3248678826b3274574e863486ea1af93f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 4 Dec 2019 14:28:18 +0000 Subject: [PATCH 788/807] Add NonNull annotations to metadata packages Also remove MetadataRenderer and SpliceInfoDecoder from the nullness blacklist PiperOrigin-RevId: 283744417 --- .../android/exoplayer2/metadata/Metadata.java | 18 ++++++--------- .../exoplayer2/metadata/MetadataRenderer.java | 23 +++++++++++-------- .../metadata/emsg/package-info.java | 19 +++++++++++++++ .../metadata/flac/package-info.java | 19 +++++++++++++++ .../exoplayer2/metadata/icy/IcyDecoder.java | 7 +++--- .../exoplayer2/metadata/icy/package-info.java | 19 +++++++++++++++ .../exoplayer2/metadata/id3/ApicFrame.java | 2 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 20 ++++++++++------ .../exoplayer2/metadata/id3/package-info.java | 19 +++++++++++++++ .../exoplayer2/metadata/package-info.java | 19 +++++++++++++++ .../metadata/scte35/SpliceInfoDecoder.java | 10 +++++--- .../metadata/scte35/package-info.java | 19 +++++++++++++++ 12 files changed, 158 insertions(+), 36 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 35702da576..046c1fef55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A collection of metadata entries. @@ -57,19 +56,15 @@ public final class Metadata implements Parcelable { * @param entries The metadata entries. */ public Metadata(Entry... entries) { - this.entries = entries == null ? new Entry[0] : entries; + this.entries = entries; } /** * @param entries The metadata entries. */ public Metadata(List entries) { - if (entries != null) { - this.entries = new Entry[entries.size()]; - entries.toArray(this.entries); - } else { - this.entries = new Entry[0]; - } + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); } /* package */ Metadata(Parcel in) { @@ -118,9 +113,10 @@ public final class Metadata implements Parcelable { * @return The metadata instance with the appended entries. */ public Metadata copyWithAppendedEntries(Entry... entriesToAppend) { - @NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length); - System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length); - return new Metadata(Util.castNonNullTypeArray(merged)); + if (entriesToAppend.length == 0) { + return this; + } + return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend)); } @Override 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 d738a8662e..5b287b0414 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; @@ -22,7 +24,6 @@ import android.os.Message; import androidx.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; @@ -30,6 +31,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A renderer for metadata. @@ -46,12 +48,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataOutput output; @Nullable private final Handler outputHandler; private final MetadataInputBuffer buffer; - private final Metadata[] pendingMetadata; + private final @NullableType Metadata[] pendingMetadata; private final long[] pendingMetadataTimestamps; private int pendingMetadataIndex; private int pendingMetadataCount; - private MetadataDecoder decoder; + @Nullable private MetadataDecoder decoder; private boolean inputStreamEnded; private long subsampleOffsetUs; @@ -98,7 +100,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { decoder = decoderFactory.createDecoder(formats[0]); } @@ -109,7 +111,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); FormatHolder formatHolder = getFormatHolder(); @@ -124,7 +126,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - Metadata metadata = decoder.decode(buffer); + @Nullable Metadata metadata = castNonNull(decoder).decode(buffer); if (metadata != null) { List entries = new ArrayList<>(metadata.length()); decodeWrappedMetadata(metadata, entries); @@ -139,12 +141,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } } else if (result == C.RESULT_FORMAT_READ) { - subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs; } } if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { - invokeRenderer(pendingMetadata[pendingMetadataIndex]); + Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]); + invokeRenderer(metadata); pendingMetadata[pendingMetadataIndex] = null; pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; pendingMetadataCount--; @@ -158,7 +161,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { */ private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { for (int i = 0; i < metadata.length(); i++) { - Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { MetadataDecoder wrappedMetadataDecoder = decoderFactory.createDecoder(wrappedMetadataFormat); @@ -167,7 +170,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); buffer.clear(); buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); - buffer.data.put(wrappedMetadataBytes); + castNonNull(buffer.data).put(wrappedMetadataBytes); buffer.flip(); @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); if (innerMetadata != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java new file mode 100644 index 0000000000..2b03ce8df3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/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.metadata.emsg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java new file mode 100644 index 0000000000..343ab232e0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/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.metadata.flac; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index 13d6b485b3..3834dce583 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.icy; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; @@ -28,8 +29,6 @@ import java.util.regex.Pattern; /** Decodes ICY stream information. */ public final class IcyDecoder implements MetadataDecoder { - private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; @@ -45,8 +44,8 @@ public final class IcyDecoder implements MetadataDecoder { @VisibleForTesting /* package */ Metadata decode(String metadata) { - String name = null; - String url = null; + @Nullable String name = null; + @Nullable String url = null; int index = 0; Matcher matcher = METADATA_ELEMENT.matcher(metadata); while (matcher.find(index)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java new file mode 100644 index 0000000000..2a2d0c7fc2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/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.metadata.icy; + +import com.google.android.exoplayer2.util.NonNullApi; 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 d4bedc63cc..3f4a400677 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 @@ -47,7 +47,7 @@ public final class ApicFrame extends Id3Frame { /* package */ ApicFrame(Parcel in) { super(ID); mimeType = castNonNull(in.readString()); - description = castNonNull(in.readString()); + description = in.readString(); pictureType = in.readInt(); pictureData = castNonNull(in.createByteArray()); } 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 ba0968cbd4..faab7f0775 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 @@ -155,7 +155,8 @@ public final class Id3Decoder implements MetadataDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. */ - private static @Nullable Id3Header decodeHeader(ParsableByteArray data) { + @Nullable + private static Id3Header decodeHeader(ParsableByteArray data) { if (data.bytesLeft() < ID3_HEADER_LENGTH) { Log.w(TAG, "Data too short to be an ID3 tag"); return null; @@ -269,7 +270,8 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable Id3Frame decodeFrame( + @Nullable + private static Id3Frame decodeFrame( int majorVersion, ParsableByteArray id3Data, boolean unsignedIntFrameSizeHack, @@ -404,8 +406,9 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable TextInformationFrame decodeTxxxFrame( - ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + @Nullable + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -427,7 +430,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame("TXXX", description, value); } - private static @Nullable TextInformationFrame decodeTextInformationFrame( + @Nullable + private static TextInformationFrame decodeTextInformationFrame( ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -446,7 +450,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } - private static @Nullable UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -557,7 +562,8 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static @Nullable CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 4) { // Frame is malformed. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java new file mode 100644 index 0000000000..8422071842 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/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.metadata.id3; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/package-info.java new file mode 100644 index 0000000000..a55cc1b6b3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/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.metadata; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 1153f918fc..0e161d9c69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.metadata.scte35; +import androidx.annotation.Nullable; 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.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Decodes splice info sections and produces splice commands. @@ -37,7 +40,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); @@ -47,6 +50,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { @@ -54,7 +59,6 @@ public final class SpliceInfoDecoder implements MetadataDecoder { timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); } - ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); sectionData.reset(data, size); @@ -68,7 +72,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader.skipBits(20); int spliceCommandLength = sectionHeader.readBits(12); int spliceCommandType = sectionHeader.readBits(8); - SpliceCommand command = null; + @Nullable SpliceCommand command = null; // Go to the start of the command by skipping all fields up to command_type. sectionData.skipBytes(14); switch (spliceCommandType) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java new file mode 100644 index 0000000000..0c4448f4d3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/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.metadata.scte35; + +import com.google.android.exoplayer2.util.NonNullApi; From e97b8347eb190f12191c5b97d1c086326387f9bb Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 4 Dec 2019 15:41:41 +0000 Subject: [PATCH 789/807] Add IntDefs for renderer capabilities. This simplifies documentation and adds compiler checks that the correct values are used. PiperOrigin-RevId: 283754163 --- .../ext/av1/Libgav1VideoRenderer.java | 8 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 2 + .../ext/flac/LibflacAudioRenderer.java | 1 + .../ext/opus/LibopusAudioRenderer.java | 1 + .../ext/vp9/LibvpxVideoRenderer.java | 8 +- library/core/build.gradle | 2 +- .../android/exoplayer2/BaseRenderer.java | 1 + .../android/exoplayer2/NoSampleRenderer.java | 4 +- .../exoplayer2/RendererCapabilities.java | 177 +++++++++++++++--- .../audio/MediaCodecAudioRenderer.java | 17 +- .../audio/SimpleDecoderAudioRenderer.java | 17 +- .../mediacodec/MediaCodecRenderer.java | 8 +- .../exoplayer2/metadata/MetadataRenderer.java | 7 +- .../android/exoplayer2/text/TextRenderer.java | 9 +- .../trackselection/DefaultTrackSelector.java | 124 ++++++------ .../trackselection/MappingTrackSelector.java | 131 +++++++------ .../android/exoplayer2/util/EventLogger.java | 14 +- .../video/MediaCodecVideoRenderer.java | 14 +- .../video/SimpleDecoderVideoRenderer.java | 6 +- .../video/spherical/CameraMotionRenderer.java | 6 +- .../audio/SimpleDecoderAudioRendererTest.java | 1 + .../DefaultTrackSelectorTest.java | 27 ++- .../MappingTrackSelectorTest.java | 11 +- .../exoplayer2/ui/TrackSelectionView.java | 3 +- .../playbacktests/gts/DashTestRunner.java | 2 +- .../exoplayer2/testutil/FakeRenderer.java | 5 +- 26 files changed, 397 insertions(+), 209 deletions(-) diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java index 81cfec29fd..3d10c2579b 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -133,16 +134,17 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { } @Override + @Capabilities protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType) || !Gav1Library.isAvailable()) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); } @Override 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 39d1ee4094..17292cec34 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,6 +92,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { Assertions.checkNotNull(format.sampleMimeType); @@ -108,6 +109,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SEAMLESS; } 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 d833c47d14..3e8d727476 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 @@ -51,6 +51,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { if (!FlacLibrary.isAvailable() 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 d17b6ebb53..3592331eff 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 @@ -83,6 +83,7 @@ public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { boolean drmIsSupported = 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 c84c3b41fe..28cb35e60f 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -223,10 +224,11 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { } @Override + @Capabilities protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } boolean drmIsSupported = format.drmInitData == null @@ -234,9 +236,9 @@ public class LibvpxVideoRenderer extends SimpleDecoderVideoRenderer { || (format.exoMediaCryptoType == null && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (!drmIsSupported) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } - return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); } @Override diff --git a/library/core/build.gradle b/library/core/build.gradle index 3cc14326c5..6e512e4c1e 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -16,7 +16,7 @@ apply from: '../../constants.gradle' android { compileSdkVersion project.ext.compileSdkVersion - + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 3cdab8baf1..bf43e74c2a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -177,6 +177,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // RendererCapabilities implementation. @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 52bf4b3d06..b0f690d3e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -185,11 +185,13 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // RendererCapabilities implementation. @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index de0d481386..95f1749f10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -15,7 +15,12 @@ */ package com.google.android.exoplayer2; +import android.annotation.SuppressLint; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.MimeTypes; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines the capabilities of a {@link Renderer}. @@ -23,10 +28,22 @@ import com.google.android.exoplayer2.util.MimeTypes; public interface RendererCapabilities { /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link + * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link + * #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FORMAT_HANDLED, + FORMAT_EXCEEDS_CAPABILITIES, + FORMAT_UNSUPPORTED_DRM, + FORMAT_UNSUPPORTED_SUBTYPE, + FORMAT_UNSUPPORTED_TYPE + }) + @interface FormatSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link FormatSupport} only. */ int FORMAT_SUPPORT_MASK = 0b111; /** * The {@link Renderer} is capable of rendering the format. @@ -72,9 +89,15 @@ public interface RendererCapabilities { int FORMAT_UNSUPPORTED_TYPE = 0b000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. + * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS}, + * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SEAMLESS, ADAPTIVE_NOT_SUPPORTED}) + @interface AdaptiveSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ int ADAPTIVE_SUPPORT_MASK = 0b11000; /** * The {@link Renderer} can seamlessly adapt between formats. @@ -91,9 +114,15 @@ public interface RendererCapabilities { int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link + * #TUNNELING_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) + @interface TunnelingSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ int TUNNELING_SUPPORT_MASK = 0b100000; /** * The {@link Renderer} supports tunneled output. @@ -104,6 +133,110 @@ public interface RendererCapabilities { */ int TUNNELING_NOT_SUPPORTED = 0b000000; + /** + * Combined renderer capabilities. + * + *

        This is a bitwise OR of {@link FormatSupport}, {@link AdaptiveSupport} and {@link + * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or + * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} + * or {@link #create(int, int, int)} to create the combined capabilities. + * + *

        Possible values: + * + *

          + *
        • {@link FormatSupport}: The level of support for the format itself. One of {@link + * #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + *
        • {@link AdaptiveSupport}: The level of support for adapting from the format to another + * format of the same mime type. One of {@link #ADAPTIVE_SEAMLESS}, {@link + * #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
        • {@link TunnelingSupport}: The level of support for tunneling. One of {@link + * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
        + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + // Intentionally empty to prevent assignment or comparison with individual flags without masking. + @IntDef({}) + @interface Capabilities {} + + /** + * Returns {@link Capabilities} for the given {@link FormatSupport}. + * + *

        The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link + * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. + * + * @param formatSupport The {@link FormatSupport}. + * @return The combined {@link Capabilities} of the given {@link FormatSupport}, {@link + * #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + */ + @Capabilities + static int create(@FormatSupport int formatSupport) { + return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED); + } + + /** + * Returns {@link Capabilities} combining the given {@link FormatSupport}, {@link AdaptiveSupport} + * and {@link TunnelingSupport}. + * + * @param formatSupport The {@link FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @return The combined {@link Capabilities}. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @Capabilities + static int create( + @FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport) { + return formatSupport | adaptiveSupport | tunnelingSupport; + } + + /** + * Returns the {@link FormatSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link FormatSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @FormatSupport + static int getFormatSupport(@Capabilities int supportFlags) { + return supportFlags & FORMAT_SUPPORT_MASK; + } + + /** + * Returns the {@link AdaptiveSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link AdaptiveSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @AdaptiveSupport + static int getAdaptiveSupport(@Capabilities int supportFlags) { + return supportFlags & ADAPTIVE_SUPPORT_MASK; + } + + /** + * Returns the {@link TunnelingSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link TunnelingSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @TunnelingSupport + static int getTunnelingSupport(@Capabilities int supportFlags) { + return supportFlags & TUNNELING_SUPPORT_MASK; + } + /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a @@ -115,39 +248,23 @@ public interface RendererCapabilities { int getTrackType(); /** - * Returns the extent to which the {@link Renderer} supports a given format. The returned value is - * the bitwise OR of three properties: - *

          - *
        • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
        • - *
        • The level of support for adapting from the format to another format of the same mime type. - * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
        • - *
        • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and - * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
        • - *
        - * The individual properties can be retrieved by performing a bitwise AND with - * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and - * {@link #TUNNELING_SUPPORT_MASK} respectively. + * Returns the extent to which the {@link Renderer} supports a given format. * * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. + * @return The {@link Capabilities} for this format. * @throws ExoPlaybackException If an error occurs. */ + @Capabilities int supportsFormat(Format format) throws ExoPlaybackException; /** * Returns the extent to which the {@link Renderer} supports adapting between supported formats - * that have different mime types. + * that have different MIME types. * - * @return The extent to which the renderer supports adapting between supported formats that have - * different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport} for adapting between supported formats that have different + * MIME types. * @throws ExoPlaybackException If an error occurs. */ + @AdaptiveSupport int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException; - } 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 a6a8b03448..3e48966c54 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -358,6 +359,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override + @Capabilities protected int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, @@ -365,8 +367,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; boolean supportsFormatDrm = format.drmInitData == null @@ -376,31 +379,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (supportsFormatDrm && allowPassthrough(format.channelCount, mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding)) || !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { // Assume the decoder outputs 16-bit PCM, unless the input is raw. - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } List decoderInfos = getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override 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 21991008cb..d5a5ffe7bb 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 @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -222,26 +223,28 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override + @Capabilities public final int supportsFormat(Format format) { if (!MimeTypes.isAudio(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } - int formatSupport = supportsFormatInternal(drmSessionManager, format); + @FormatSupport int formatSupport = supportsFormatInternal(drmSessionManager, format); if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { - return formatSupport; + return RendererCapabilities.create(formatSupport); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } /** - * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link - * #supportsFormat(Format)}. + * Returns the {@link FormatSupport} for the given {@link Format}. * * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format, which has an audio {@link Format#sampleMimeType}. - * @return The extent to which the renderer supports the format itself. + * @return The {@link FormatSupport} for this {@link Format}. */ + @FormatSupport protected abstract int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format); 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 1361bb6ad4..e8501dad75 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 @@ -452,11 +452,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override + @Capabilities public final int supportsFormat(Format format) throws ExoPlaybackException { try { return supportsFormat(mediaCodecSelector, drmSessionManager, format); @@ -466,15 +468,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns the extent to which the renderer is capable of supporting a given {@link Format}. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param mediaCodecSelector The decoder selector. * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The {@link Format}. - * @return The extent to which the renderer is capable of supporting the given format. See {@link - * #supportsFormat(Format)} for more detail. + * @return The {@link Capabilities} for this {@link Format}. * @throws DecoderQueryException If there was an error querying decoders. */ + @Capabilities protected abstract int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, 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 5b287b0414..7a5235a466 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -91,11 +92,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } 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 1622d68d99..35e60dcf82 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -118,13 +119,15 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else if (MimeTypes.isText(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } 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 437546559c..9982ce5369 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 @@ -30,6 +30,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -1608,8 +1611,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1678,18 +1681,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { * generated by this method will be overridden to account for these properties. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no * selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection.@NullableType Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1793,10 +1796,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a video renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was @@ -1806,8 +1809,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected TrackSelection.Definition selectVideoTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -1827,8 +1830,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectAdaptiveVideoTrack( TrackGroupArray groups, - int[][] formatSupport, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupport, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params) { int requiredAdaptiveSupport = params.allowVideoNonSeamlessAdaptiveness @@ -1861,7 +1864,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveVideoTracksForGroup( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, @@ -1926,7 +1929,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1954,7 +1957,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void filterAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1981,7 +1984,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveVideoTrack( Format format, @Nullable String mimeType, - int formatSupport, + @Capabilities int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, @@ -1998,7 +2001,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectFixedVideoTrack( - TrackGroupArray groups, int[][] formatSupports, Parameters params) { + TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -2008,7 +2011,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2071,10 +2074,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for an audio renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or @@ -2085,8 +2088,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectAudioTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -2095,7 +2098,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2148,7 +2151,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveAudioTracks( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, boolean allowMixedSampleRateAdaptiveness, @@ -2202,7 +2205,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveAudioTrackCount( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2226,7 +2229,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveAudioTrack( Format format, - int formatSupport, + @Capabilities int formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2252,8 +2255,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a text renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (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 text track declares no language or no text track was selected. @@ -2264,7 +2267,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectTextTrack( TrackGroupArray groups, - int[][] formatSupport, + @Capabilities int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { @@ -2273,7 +2276,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2305,22 +2308,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param trackType The type of the renderer. * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable protected TrackSelection.Definition selectOtherTrack( - int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) + int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2351,6 +2354,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderers if so. * * @param mappedTrackInfo Mapped track information. + * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param rendererConfigurations The renderer configurations. Configurations may be replaced with * ones that enable tunneling as a result of this call. * @param trackSelections The renderer track selections. @@ -2359,7 +2364,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - int[][][] renderererFormatSupports, + @Capabilities int[][][] renderererFormatSupports, @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { @@ -2408,21 +2413,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns whether a renderer supports tunneling for a {@link TrackSelection}. * - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each track, - * indexed by group index and track index (in that order). + * @param formatSupports The {@link Capabilities} for each track, indexed by group index and track + * index (in that order). * @param trackGroups The {@link TrackGroupArray}s for the renderer. * @param selection The track selection. * @return Whether the renderer supports tunneling for the {@link TrackSelection}. */ private static boolean rendererSupportsTunneling( - int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { + @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { if (selection == null) { return false; } int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); for (int i = 0; i < selection.length(); i++) { + @Capabilities int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; - if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) + if (RendererCapabilities.getTunnelingSupport(trackFormatSupport) != RendererCapabilities.TUNNELING_SUPPORTED) { return false; } @@ -2446,20 +2452,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from - * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is - * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set - * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link + * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the + * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. * - * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}. - * @param allowExceedsCapabilities Whether to return true if the format support component of the - * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. - * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or - * if {@code allowExceedsCapabilities} is set and the format support component is - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @param formatSupport {@link Capabilities}. + * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if + * {@code allowExceedsCapabilities} is set and the format support is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. */ - protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { - int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + protected static boolean isSupported( + @Capabilities int formatSupport, boolean allowExceedsCapabilities) { + @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport); return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } @@ -2615,7 +2621,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final int sampleRate; private final int bitrate; - public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { + public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) { this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); @@ -2754,7 +2760,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public TextTrackScore( Format format, Parameters parameters, - int trackFormatSupport, + @Capabilities int trackFormatSupport, @Nullable String selectedAudioLanguage) { isWithinRendererCapabilities = isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); 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 425da6c1c4..9c6b2409c3 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 @@ -22,6 +22,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -90,25 +93,25 @@ public abstract class MappingTrackSelector extends TrackSelector { private final int rendererCount; private final int[] rendererTrackTypes; private final TrackGroupArray[] rendererTrackGroups; - private final int[] rendererMixedMimeTypeAdaptiveSupports; - private final int[][][] rendererFormatSupports; + @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports; + @Capabilities private final int[][][] rendererFormatSupports; private final TrackGroupArray unmappedTrackGroups; /** * @param rendererTrackTypes The track type handled by each renderer. * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer. - * @param rendererMixedMimeTypeAdaptiveSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. */ @SuppressWarnings("deprecation") /* package */ MappedTrackInfo( int[] rendererTrackTypes, TrackGroupArray[] rendererTrackGroups, - int[] rendererMixedMimeTypeAdaptiveSupports, - int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports, + @Capabilities int[][][] rendererFormatSupports, TrackGroupArray unmappedTrackGroups) { this.rendererTrackTypes = rendererTrackTypes; this.rendererTrackGroups = rendererTrackGroups; @@ -149,25 +152,28 @@ public abstract class MappingTrackSelector extends TrackSelector { * Returns the extent to which a renderer can play the tracks that are mapped to it. * * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; - for (int[] trackGroupFormatSupport : rendererFormatSupport) { - for (int trackFormatSupport : trackGroupFormatSupport) { + @RendererSupport + public int getRendererSupport(int rendererIndex) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; + for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) { + for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) { int trackRendererSupport; - switch (trackFormatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) { + switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) { case RendererCapabilities.FORMAT_HANDLED: return RENDERER_SUPPORT_PLAYABLE_TRACKS; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; break; - default: + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; break; + default: + throw new IllegalStateException(); } bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); } @@ -177,7 +183,8 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTypeSupport(int)}. */ @Deprecated - public @RendererSupport int getTrackTypeRendererSupport(int trackType) { + @RendererSupport + public int getTrackTypeRendererSupport(int trackType) { return getTypeSupport(trackType); } @@ -188,12 +195,11 @@ public abstract class MappingTrackSelector extends TrackSelector { * returned. * * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getTypeSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @RendererSupport + public int getTypeSupport(int trackType) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; for (int i = 0; i < rendererCount; i++) { if (rendererTrackTypes[i] == trackType) { bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); @@ -204,6 +210,7 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */ @Deprecated + @FormatSupport public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { return getTrackSupport(rendererIndex, groupIndex, trackIndex); } @@ -214,15 +221,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param rendererIndex The renderer index. * @param groupIndex The index of the track group to which the track belongs. * @param trackIndex The index of the track within the track group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, {@link - * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + * @return The {@link FormatSupport}. */ + @FormatSupport public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { - return rendererFormatSupports[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; + return RendererCapabilities.getFormatSupport( + rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); } /** @@ -242,10 +246,9 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param groupIndex The index of the track group. * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the * renderer are included when determining support. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport( int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length; @@ -253,7 +256,7 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] trackIndices = new int[trackCount]; int trackIndexCount = 0; for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); + @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); if (fixedSupport == RendererCapabilities.FORMAT_HANDLED || (includeCapabilitiesExceededTracks && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { @@ -270,13 +273,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; boolean multipleMimeTypes = false; String firstSampleMimeType = null; for (int i = 0; i < trackIndices.length; i++) { @@ -291,8 +293,8 @@ public abstract class MappingTrackSelector extends TrackSelector { adaptiveSupport = Math.min( adaptiveSupport, - rendererFormatSupports[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + RendererCapabilities.getAdaptiveSupport( + rendererFormatSupports[rendererIndex][groupIndex][i])); } return multipleMimeTypes ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex]) @@ -341,13 +343,14 @@ public abstract class MappingTrackSelector extends TrackSelector { // any renderer. int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1]; TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][]; - int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; + @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; for (int i = 0; i < rendererTrackGroups.length; i++) { rendererTrackGroups[i] = new TrackGroup[trackGroups.length]; rendererFormatSupports[i] = new int[trackGroups.length][]; } // Determine the extent to which each renderer supports mixed mimeType adaptation. + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports = getMixedMimeTypeAdaptationSupports(rendererCapabilities); @@ -358,8 +361,11 @@ public abstract class MappingTrackSelector extends TrackSelector { // Associate the group to a preferred renderer. int rendererIndex = findRenderer(rendererCapabilities, group); // Evaluate the support that the renderer provides for each track in the group. - int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length - ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group); + @Capabilities + int[] rendererFormatSupport = + rendererIndex == rendererCapabilities.length + ? new int[group.length] + : getFormatSupport(rendererCapabilities[rendererIndex], group); // Stash the results. int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex]; rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group; @@ -406,10 +412,10 @@ public abstract class MappingTrackSelector extends TrackSelector { * Given mapped track information, returns a track selection and configuration for each renderer. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupport The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return A pair consisting of the track selections and configurations for each renderer. A null * configuration indicates the renderer should be disabled, in which case the track selection * will also be null. A track selection may also be null for a non-disabled renderer if {@link @@ -419,8 +425,8 @@ public abstract class MappingTrackSelector extends TrackSelector { protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** @@ -446,12 +452,14 @@ public abstract class MappingTrackSelector extends TrackSelector { private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) throws ExoPlaybackException { int bestRendererIndex = rendererCapabilities.length; - int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) { RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)) - & RendererCapabilities.FORMAT_SUPPORT_MASK; + @FormatSupport + int formatSupportLevel = + RendererCapabilities.getFormatSupport( + rendererCapability.supportsFormat(group.getFormat(trackIndex))); if (formatSupportLevel > bestFormatSupportLevel) { bestRendererIndex = rendererIndex; bestFormatSupportLevel = formatSupportLevel; @@ -466,18 +474,18 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified - * {@link TrackGroup}, returning the results in an array. + * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link + * TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. * @param group The track group to evaluate. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsFormat} for each track in the group. + * @return An array containing {@link Capabilities} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. */ + @Capabilities private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group) throws ExoPlaybackException { - int[] formatSupport = new int[group.length]; + @Capabilities int[] formatSupport = new int[group.length]; for (int i = 0; i < group.length; i++) { formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i)); } @@ -489,13 +497,14 @@ public abstract class MappingTrackSelector extends TrackSelector { * returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @return An array containing the result of calling {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the + * renderer. * @throws ExoPlaybackException If an error occurs determining the adaptation support. */ + @AdaptiveSupport private static int[] getMixedMimeTypeAdaptationSupports( RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException { - int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; + @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) { mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation(); } 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 a4e8e311ca..6caf549afe 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 @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -210,7 +212,8 @@ public class EventLogger implements AnalyticsListener { String adaptiveSupport = getAdaptiveSupportString( trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)); logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); @@ -552,7 +555,7 @@ public class EventLogger implements AnalyticsListener { } } - private static String getFormatSupportString(int formatSupport) { + private static String getFormatSupportString(@FormatSupport int formatSupport) { switch (formatSupport) { case RendererCapabilities.FORMAT_HANDLED: return "YES"; @@ -565,11 +568,12 @@ public class EventLogger implements AnalyticsListener { case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: return "NO"; default: - return "?"; + throw new IllegalStateException(); } } - private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { + private static String getAdaptiveSupportString( + int trackCount, @AdaptiveSupport int adaptiveSupport) { if (trackCount < 2) { return "N/A"; } @@ -581,7 +585,7 @@ public class EventLogger implements AnalyticsListener { case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: return "NO"; default: - return "?"; + throw new IllegalStateException(); } } 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 38ac80bf26..57c3ab13fa 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 @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -360,6 +361,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override + @Capabilities protected int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, @@ -367,7 +369,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Nullable DrmInitData drmInitData = format.drmInitData; // Assume encrypted content requires secure decoders. @@ -388,7 +390,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /* requiresTunnelingDecoder= */ false); } if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } boolean supportsFormatDrm = drmInitData == null @@ -396,16 +398,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || (format.exoMediaCryptoType == null && supportsFormatDrm(drmSessionManager, drmInitData)); if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - int tunnelingSupport = TUNNELING_NOT_SUPPORTED; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = getDecoderInfos( @@ -421,8 +424,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 73c964d1fe..9aa50e4388 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -157,6 +157,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { // BaseRenderer implementation. @Override + @Capabilities public final int supportsFormat(Format format) { return supportsFormatInternal(drmSessionManager, format); } @@ -498,13 +499,14 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } /** - * Returns the extent to which the subclass supports a given format. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format, which has a video {@link Format#sampleMimeType}. - * @return The extent to which the subclass supports the format itself. + * @return The {@link Capabilities} for this {@link Format}. * @see RendererCapabilities#supportsFormat(Format) */ + @Capabilities protected abstract int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format); 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 d1cf0abc56..35804adbe3 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 @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -48,10 +49,11 @@ public class CameraMotionRenderer extends BaseRenderer { } @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType) - ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override 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 6769f5049b..f8fd2fc9ca 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 @@ -58,6 +58,7 @@ public class SimpleDecoderAudioRendererTest { audioRenderer = new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { return FORMAT_HANDLED; 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 292742b527..62d38187c4 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 @@ -1767,7 +1767,7 @@ public final class DefaultTrackSelectorTest { private static final class FakeRendererCapabilities implements RendererCapabilities { private final int trackType; - private final int supportValue; + @Capabilities private final int supportValue; /** * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all @@ -1777,19 +1777,21 @@ public final class DefaultTrackSelectorTest { * support for. */ FakeRendererCapabilities(int trackType) { - this(trackType, FORMAT_HANDLED | ADAPTIVE_SEAMLESS); + this( + trackType, + RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED)); } /** - * Returns {@link FakeRendererCapabilities} that advertises support level using given value - * for all tracks of the given type. + * Returns {@link FakeRendererCapabilities} that advertises support level using given value for + * all tracks of the given type. * * @param trackType the track type of all formats that this renderer capabilities advertises - * support for. - * @param supportValue the support level value that will be returned for formats with - * the given type. + * support for. + * @param supportValue the {@link Capabilities} that will be returned for formats with the given + * type. */ - FakeRendererCapabilities(int trackType, int supportValue) { + FakeRendererCapabilities(int trackType, @Capabilities int supportValue) { this.trackType = trackType; this.supportValue = supportValue; } @@ -1800,12 +1802,15 @@ public final class DefaultTrackSelectorTest { } @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.getTrackType(format.sampleMimeType) == trackType - ? (supportValue) : FORMAT_UNSUPPORTED_TYPE; + ? supportValue + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } @@ -1841,13 +1846,15 @@ public final class DefaultTrackSelectorTest { } @Override + @Capabilities public int supportsFormat(Format format) { return format.id != null && formatToCapability.containsKey(format.id) ? formatToCapability.get(format.id) - : FORMAT_UNSUPPORTED_TYPE; + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index efb828fc57..f7bfc24881 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -23,6 +23,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -112,8 +114,8 @@ public final class MappingTrackSelectorTest { @Override protected Pair selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); lastMappedTrackInfo = mappedTrackInfo; @@ -148,12 +150,15 @@ public final class MappingTrackSelectorTest { } @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { return MimeTypes.getTrackType(format.sampleMimeType) == trackType - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_SEAMLESS; } 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 79990e53a6..1e2d226fd6 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 @@ -371,7 +371,8 @@ public class TrackSelectionView extends LinearLayout { private boolean shouldEnableAdaptiveSelection(int groupIndex) { return allowAdaptiveSelections && trackGroups.get(groupIndex).length > 1 - && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) + && mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; } 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 8323d66614..5deed11699 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 @@ -451,7 +451,7 @@ import java.util.List; } private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + return RendererCapabilities.getFormatSupport(formatSupport) == RendererCapabilities.FORMAT_HANDLED; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index 39d3d8f7f4..987a9e33c1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; @@ -110,9 +111,11 @@ public class FakeRenderer extends BaseRenderer { } @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } /** Called when the renderer reads a new format. */ From 9f44e902b14fdb1820f72ce55884efb932da0d8c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 4 Dec 2019 19:03:35 +0000 Subject: [PATCH 790/807] Fix incorrect DvbParser assignment PiperOrigin-RevId: 283791815 --- .../java/com/google/android/exoplayer2/text/dvb/DvbParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 3f2fef454f..0e41e4d1b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -645,7 +645,7 @@ import java.util.List; clutMapTable2To8 = buildClutMapTable(4, 8, data); break; case DATA_TYPE_48_TABLE_DATA: - clutMapTable2To8 = buildClutMapTable(16, 8, data); + clutMapTable4To8 = buildClutMapTable(16, 8, data); break; case DATA_TYPE_END_LINE: column = horizontalAddress; From cab05cb71de25a3af36048d91c47c61f9c2c0f17 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 4 Dec 2019 20:29:56 +0000 Subject: [PATCH 791/807] Two minor nullability fixes PiperOrigin-RevId: 283810554 --- .../google/android/exoplayer2/drm/DefaultDrmSession.java | 7 ++++--- .../android/exoplayer2/extractor/MpegAudioHeader.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) 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 0d93ec7c62..432cc6613f 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 @@ -41,7 +41,6 @@ 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; @@ -122,8 +121,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Nullable private RequestHandler requestHandler; @Nullable private T mediaCrypto; @Nullable private DrmSessionException lastException; - private byte @NullableType [] sessionId; - private byte @MonotonicNonNull [] offlineLicenseKeySetId; + @Nullable private byte[] sessionId; + @MonotonicNonNull private byte[] offlineLicenseKeySetId; @Nullable private KeyRequest currentKeyRequest; @Nullable private ProvisionRequest currentProvisionRequest; @@ -148,6 +147,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning * requests. */ + // the constructor does not initialize fields: sessionId + @SuppressWarnings("nullness:initialization.fields.uninitialized") public DefaultDrmSession( UUID uuid, ExoMediaDrm mediaDrm, 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 8412b738bb..04d85b8bc5 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 @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; -import org.checkerframework.checker.nullness.qual.Nullable; /** * An MPEG audio frame header. From e10a78e6b7a2ccfe8d21d460b08fcfdf5fec4100 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 5 Dec 2019 13:02:01 +0000 Subject: [PATCH 792/807] Add NonNull annotations to text packages PiperOrigin-RevId: 283951181 --- .../google/android/exoplayer2/text/Cue.java | 2 +- .../text/SimpleSubtitleDecoder.java | 8 +- .../android/exoplayer2/text/TextRenderer.java | 12 +- .../exoplayer2/text/cea/Cea708Cue.java | 6 - .../exoplayer2/text/cea/Cea708Decoder.java | 4 +- .../exoplayer2/text/cea/package-info.java | 19 +++ .../exoplayer2/text/dvb/DvbParser.java | 131 +++++++++++------- .../exoplayer2/text/dvb/package-info.java | 19 +++ .../android/exoplayer2/text/package-info.java | 19 +++ .../exoplayer2/text/ssa/SsaDecoder.java | 25 ++-- .../exoplayer2/text/ssa/package-info.java | 19 +++ .../exoplayer2/text/subrip/SubripDecoder.java | 4 +- .../exoplayer2/text/ttml/package-info.java | 19 +++ .../text/webvtt/WebvttCssStyle.java | 4 +- .../exoplayer2/text/webvtt/WebvttCue.java | 4 +- .../text/webvtt/WebvttCueParser.java | 8 +- .../text/webvtt/WebvttParserUtil.java | 4 +- 17 files changed, 215 insertions(+), 92 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java 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 bd617ad626..946af76e53 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 @@ -333,7 +333,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, 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 bd561afaf8..8a1aea179a 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 @@ -18,6 +18,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 com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -29,9 +30,8 @@ public abstract class SimpleSubtitleDecoder extends private final String name; - /** - * @param name The name of the decoder. - */ + /** @param name The name of the decoder. */ + @SuppressWarnings("initialization:method.invocation.invalid") protected SimpleSubtitleDecoder(String name) { super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]); this.name = name; @@ -74,7 +74,7 @@ public abstract class SimpleSubtitleDecoder extends protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Assertions.checkNotNull(inputBuffer.data); Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). 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 35e60dcf82..d359eebfdb 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 @@ -80,11 +80,11 @@ public final class TextRenderer extends BaseRenderer implements Callback { private boolean inputStreamEnded; private boolean outputStreamEnded; @ReplacementState private int decoderReplacementState; - private Format streamFormat; - private SubtitleDecoder decoder; - private SubtitleInputBuffer nextInputBuffer; - private SubtitleOutputBuffer subtitle; - private SubtitleOutputBuffer nextSubtitle; + @Nullable private Format streamFormat; + @Nullable private SubtitleDecoder decoder; + @Nullable private SubtitleInputBuffer nextInputBuffer; + @Nullable private SubtitleOutputBuffer subtitle; + @Nullable private SubtitleOutputBuffer nextSubtitle; private int nextSubtitleEventIndex; /** @@ -132,7 +132,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java index fc1f0e2bdc..e04094a8dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -24,11 +24,6 @@ import com.google.android.exoplayer2.text.Cue; */ /* package */ final class Cea708Cue extends Cue implements Comparable { - /** - * An unset priority. - */ - public static final int PRIORITY_UNSET = -1; - /** * The priority of the cue box. */ @@ -64,5 +59,4 @@ import com.google.android.exoplayer2.text.Cue; } return 0; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index b3be88b851..4391bc0bf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -25,6 +25,7 @@ import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; @@ -152,7 +153,8 @@ public final class Cea708Decoder extends CeaDecoder { private DtvCcPacket currentDtvCcPacket; private int currentWindow; - public Cea708Decoder(int accessibilityChannel, List initializationData) { + // TODO: Retrieve isWideAspectRatio from initializationData and use it. + public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) { ccData = new ParsableByteArray(); serviceBlockPacket = new ParsableBitArray(); selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java new file mode 100644 index 0000000000..cbdf178b6a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/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.text.cea; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 0e41e4d1b6..8382d9d9d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -22,6 +22,7 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses {@link Cue}s from a DVB subtitle bitstream. @@ -85,7 +87,7 @@ import java.util.List; private final ClutDefinition defaultClutDefinition; private final SubtitleService subtitleService; - private Bitmap bitmap; + @MonotonicNonNull private Bitmap bitmap; /** * Construct an instance for the given subtitle and ancillary page ids. @@ -131,7 +133,8 @@ import java.util.List; parseSubtitlingSegment(dataBitArray, subtitleService); } - if (subtitleService.pageComposition == null) { + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { return Collections.emptyList(); } @@ -147,7 +150,7 @@ import java.util.List; // Build the cues. List cues = new ArrayList<>(); - SparseArray pageRegions = subtitleService.pageComposition.regions; + SparseArray pageRegions = pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { // Save clean clipping state. canvas.save(); @@ -182,7 +185,7 @@ import java.util.List; objectData = subtitleService.ancillaryObjects.get(objectId); } if (objectData != null) { - Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, baseHorizontalAddress + regionObject.horizontalPosition, baseVerticalAddress + regionObject.verticalPosition, paint, canvas); @@ -248,7 +251,7 @@ import java.util.List; break; case SEGMENT_TYPE_PAGE_COMPOSITION: if (pageId == service.subtitlePageId) { - PageComposition current = service.pageComposition; + @Nullable PageComposition current = service.pageComposition; PageComposition pageComposition = parsePageComposition(data, dataFieldLength); if (pageComposition.state != PAGE_STATE_NORMAL) { service.pageComposition = pageComposition; @@ -261,11 +264,15 @@ import java.util.List; } break; case SEGMENT_TYPE_REGION_COMPOSITION: - PageComposition pageComposition = service.pageComposition; + @Nullable PageComposition pageComposition = service.pageComposition; if (pageId == service.subtitlePageId && pageComposition != null) { RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); if (pageComposition.state == PAGE_STATE_NORMAL) { - regionComposition.mergeFrom(service.regions.get(regionComposition.id)); + @Nullable + RegionComposition existingRegionComposition = service.regions.get(regionComposition.id); + if (existingRegionComposition != null) { + regionComposition.mergeFrom(existingRegionComposition); + } } service.regions.put(regionComposition.id, regionComposition); } @@ -470,8 +477,8 @@ import java.util.List; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - byte[] topFieldData = null; - byte[] bottomFieldData = null; + @Nullable byte[] topFieldData = null; + @Nullable byte[] bottomFieldData = null; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); @@ -577,11 +584,15 @@ import java.util.List; // Static drawing. - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition, - int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlocks( + ObjectData objectData, + ClutDefinition clutDefinition, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { int[] clutEntries; if (regionDepth == REGION_DEPTH_8_BIT) { clutEntries = clutDefinition.clutEntries8Bit; @@ -596,23 +607,27 @@ import java.util.List; verticalAddress + 1, paint, canvas); } - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, - int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlock( + byte[] pixelData, + int[] clutEntries, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { ParsableBitArray data = new ParsableBitArray(pixelData); int column = horizontalAddress; int line = verticalAddress; - byte[] clutMapTable2To4 = null; - byte[] clutMapTable2To8 = null; - byte[] clutMapTable4To8 = null; + @Nullable byte[] clutMapTable2To4 = null; + @Nullable byte[] clutMapTable2To8 = null; + @Nullable byte[] clutMapTable4To8 = null; while (data.bitsLeft() != 0) { int dataType = data.readBits(8); switch (dataType) { case DATA_TYPE_2BP_CODE_STRING: - byte[] clutMapTable2ToX; + @Nullable byte[] clutMapTable2ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; } else if (regionDepth == REGION_DEPTH_4_BIT) { @@ -625,7 +640,7 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_4BP_CODE_STRING: - byte[] clutMapTable4ToX; + @Nullable byte[] clutMapTable4ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; } else { @@ -636,7 +651,9 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_8BP_CODE_STRING: - column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + column = + paint8BitPixelCodeString( + data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas); break; case DATA_TYPE_24_TABLE_DATA: clutMapTable2To4 = buildClutMapTable(4, 4, data); @@ -658,11 +675,15 @@ import java.util.List; } } - /** - * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint2BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -706,11 +727,15 @@ import java.util.List; return column; } - /** - * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint4BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -760,11 +785,15 @@ import java.util.List; return column; } - /** - * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint8BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -816,18 +845,23 @@ import java.util.List; public final int subtitlePageId; public final int ancillaryPageId; - public final SparseArray regions = new SparseArray<>(); - public final SparseArray cluts = new SparseArray<>(); - public final SparseArray objects = new SparseArray<>(); - public final SparseArray ancillaryCluts = new SparseArray<>(); - public final SparseArray ancillaryObjects = new SparseArray<>(); + public final SparseArray regions; + public final SparseArray cluts; + public final SparseArray objects; + public final SparseArray ancillaryCluts; + public final SparseArray ancillaryObjects; - public DisplayDefinition displayDefinition; - public PageComposition pageComposition; + @Nullable public DisplayDefinition displayDefinition; + @Nullable public PageComposition pageComposition; public SubtitleService(int subtitlePageId, int ancillaryPageId) { this.subtitlePageId = subtitlePageId; this.ancillaryPageId = ancillaryPageId; + regions = new SparseArray<>(); + cluts = new SparseArray<>(); + objects = new SparseArray<>(); + ancillaryCluts = new SparseArray<>(); + ancillaryObjects = new SparseArray<>(); } public void reset() { @@ -944,9 +978,6 @@ import java.util.List; } public void mergeFrom(RegionComposition otherRegionComposition) { - if (otherRegionComposition == null) { - return; - } SparseArray otherRegionObjects = otherRegionComposition.regionObjects; for (int i = 0; i < otherRegionObjects.size(); i++) { regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java new file mode 100644 index 0000000000..e5ec87a1a5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/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.text.dvb; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/package-info.java new file mode 100644 index 0000000000..5c5b3bbc31 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/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.text; + +import com.google.android.exoplayer2.util.NonNullApi; 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 d751772879..45d4554bb7 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,6 +15,8 @@ */ package com.google.android.exoplayer2.text.ssa; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.text.Layout; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -115,7 +117,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. */ private void parseHeader(ParsableByteArray data) { - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { if ("[Script Info]".equalsIgnoreCase(currentLine)) { parseScriptInfo(data); @@ -140,7 +142,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * set to the beginning of of the first line after {@code [Script Info]}. */ private void parseScriptInfo(ParsableByteArray data) { - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { String[] infoNameAndValue = currentLine.split(":"); @@ -176,9 +178,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * at the beginning of of the first line after {@code [V4+ Styles]}. */ private static Map parseStyles(ParsableByteArray data) { - SsaStyle.Format formatInfo = null; Map styles = new LinkedHashMap<>(); - String currentLine; + @Nullable SsaStyle.Format formatInfo = null; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { @@ -188,7 +190,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); continue; } - SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + @Nullable SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); if (style != null) { styles.put(style.name, style); } @@ -205,8 +207,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + @Nullable SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { format = SsaDialogueFormat.fromFormatLine(currentLine); @@ -250,6 +253,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return; } + @Nullable SsaStyle style = styles != null && format.styleIndex != C.INDEX_UNSET ? styles.get(lineValues[format.styleIndex].trim()) @@ -281,10 +285,11 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { if (!matcher.matches()) { return C.TIME_UNSET; } - long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second. + long timestampUs = + Long.parseLong(castNonNull(matcher.group(1))) * 60 * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(2))) * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(3))) * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(4))) * 10000; // 100ths of a second. return timestampUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java new file mode 100644 index 0000000000..cdf891d016 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/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.text.ssa; + +import com.google.android.exoplayer2.util.NonNullApi; 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 20b7efe50a..0c402ac018 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 @@ -73,8 +73,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); - String currentLine; + @Nullable String currentLine; while ((currentLine = subripData.readLine()) != null) { if (currentLine.length() == 0) { // Skip blank lines. @@ -119,7 +119,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Spanned text = Html.fromHtml(textBuilder.toString()); - String alignmentTag = null; + @Nullable String alignmentTag = null; for (int i = 0; i < tags.size(); i++) { String tag = tags.get(i); if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java new file mode 100644 index 0000000000..5b0685e24c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/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.text.ttml; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 9186455702..97c0acb1ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -220,7 +220,7 @@ public final class WebvttCssStyle { return fontFamily; } - public WebvttCssStyle setFontFamily(String fontFamily) { + public WebvttCssStyle setFontFamily(@Nullable String fontFamily) { this.fontFamily = Util.toLowerInvariant(fontFamily); return this; } @@ -264,7 +264,7 @@ public final class WebvttCssStyle { return textAlign; } - public WebvttCssStyle setTextAlign(Layout.Alignment textAlign) { + public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) { this.textAlign = textAlign; return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index eae879c21b..bfa067e322 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -26,9 +26,7 @@ import com.google.android.exoplayer2.util.Log; import java.lang.annotation.Documented; import java.lang.annotation.Retention; -/** - * A representation of a WebVTT cue. - */ +/** A representation of a WebVTT cue. */ public final class WebvttCue extends Cue { private static final float DEFAULT_POSITION = 0.5f; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index f587d70e90..6e5bd31b4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -44,9 +44,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) - */ +/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern @@ -94,7 +92,7 @@ public final class WebvttCueParser { */ public boolean parseCue( ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { - String firstLine = webvttData.readLine(); + @Nullable String firstLine = webvttData.readLine(); if (firstLine == null) { return false; } @@ -104,7 +102,7 @@ public final class WebvttCueParser { return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); } // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); + @Nullable String secondLine = webvttData.readLine(); if (secondLine == null) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index dce8f8157f..9075083111 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -52,7 +52,7 @@ public final class WebvttParserUtil { * @param input The input from which the line should be read. */ public static boolean isWebvttHeaderLine(ParsableByteArray input) { - String line = input.readLine(); + @Nullable String line = input.readLine(); return line != null && line.startsWith(WEBVTT_HEADER); } @@ -101,7 +101,7 @@ public final class WebvttParserUtil { */ @Nullable public static Matcher findNextCueHeader(ParsableByteArray input) { - String line; + @Nullable String line; while ((line = input.readLine()) != null) { if (COMMENT.matcher(line).matches()) { // Skip until the end of the comment block. From eb5016a6ffda33a8dc7adffd10341c2f5ac9edfc Mon Sep 17 00:00:00 2001 From: samrobinson Date: Thu, 5 Dec 2019 14:04:43 +0000 Subject: [PATCH 793/807] Fix MCR comment line break. PiperOrigin-RevId: 283958680 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e8501dad75..50b3ab8a0e 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 @@ -715,8 +715,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { 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 + // may also reach the end of the stream. Note that readSource will not read a sample into a // flags-only buffer. readToFlagsOnlyBuffer(/* requireFormat= */ false); } From 1e609e245b6510d7cac637632ead936c5fb62dc9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 5 Dec 2019 14:59:42 +0000 Subject: [PATCH 794/807] Add format and renderer support to renderer exceptions. This makes the exception easier to interpret and helps with debugging of externally reported issues. PiperOrigin-RevId: 283965317 --- RELEASENOTES.md | 1 + .../android/exoplayer2/BaseRenderer.java | 37 ++++++++++-- .../exoplayer2/ExoPlaybackException.java | 57 +++++++++++++++++-- .../exoplayer2/ExoPlayerImplInternal.java | 16 +++++- .../exoplayer2/RendererCapabilities.java | 23 ++++++++ .../audio/MediaCodecAudioRenderer.java | 52 ++++++++++------- .../audio/SimpleDecoderAudioRenderer.java | 11 ++-- .../mediacodec/MediaCodecRenderer.java | 19 +++---- .../android/exoplayer2/text/TextRenderer.java | 4 +- .../android/exoplayer2/util/EventLogger.java | 52 +++-------------- .../google/android/exoplayer2/util/Util.java | 27 +++++++++ .../video/SimpleDecoderVideoRenderer.java | 6 +- 12 files changed, 205 insertions(+), 100 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b1b39b75b1..c9564e2d58 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,7 @@ * Add `MediaPeriod.isLoading` to improve `Player.isLoading` state. * Fix issue where player errors are thrown too early at playlist transitions ([#5407](https://github.com/google/ExoPlayer/issues/5407)). + * Add `Format` and renderer support flags to renderer `ExoPlaybackException`s. * DRM: * Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`. This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index bf43e74c2a..10573af419 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -44,6 +44,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private long streamOffsetUs; private long readingPositionUs; private boolean streamIsFinal; + private boolean throwRendererExceptionIsExecuting; /** * @param trackType The track type that the renderer handles. One of the {@link C} @@ -314,8 +315,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Nullable DrmSession newSourceDrmSession = null; if (newFormat.drmInitData != null) { if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); + throw createRendererException( + new IllegalStateException("Media requires a DrmSessionManager"), newFormat); } newSourceDrmSession = drmSessionManager.acquireSession( @@ -334,6 +335,30 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for + * this renderer. + * + * @param cause The cause of the exception. + * @param format The current format used by the renderer. May be null. + */ + protected final ExoPlaybackException createRendererException( + Exception cause, @Nullable Format format) { + @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED; + if (format != null && !throwRendererExceptionIsExecuting) { + // Prevent recursive re-entry from subclass supportsFormat implementations. + throwRendererExceptionIsExecuting = true; + try { + formatSupport = RendererCapabilities.getFormatSupport(supportsFormat(format)); + } catch (ExoPlaybackException e) { + // Ignore, we are already failing. + } finally { + throwRendererExceptionIsExecuting = false; + } + } + return ExoPlaybackException.createForRenderer(cause, getIndex(), format, formatSupport); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -341,16 +366,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @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. + * 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. * @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. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { + protected final int readSource( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { 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 49aacd9638..653b6002d9 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2; import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -74,6 +75,19 @@ public final class ExoPlaybackException extends Exception { */ public final int rendererIndex; + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the {@link Format} the renderer was using + * at the time of the exception, or null if the renderer wasn't using a {@link Format}. + */ + @Nullable public final Format rendererFormat; + + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the level of {@link FormatSupport} of the + * renderer for {@link #rendererFormat}. If {@link #rendererFormat} is null, this is {@link + * RendererCapabilities#FORMAT_HANDLED}. + */ + @FormatSupport public final int rendererFormatSupport; + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ public final long timestampMs; @@ -86,7 +100,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForSource(IOException cause) { - return new ExoPlaybackException(TYPE_SOURCE, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_SOURCE, cause); } /** @@ -94,10 +108,23 @@ public final class ExoPlaybackException extends Exception { * * @param cause The cause of the failure. * @param rendererIndex The index of the renderer in which the failure occurred. + * @param rendererFormat The {@link Format} the renderer was using at the time of the exception, + * or null if the renderer wasn't using a {@link Format}. + * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code + * rendererFormat}. Ignored if {@code rendererFormat} is null. * @return The created instance. */ - public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) { - return new ExoPlaybackException(TYPE_RENDERER, cause, rendererIndex); + public static ExoPlaybackException createForRenderer( + Exception cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + return new ExoPlaybackException( + TYPE_RENDERER, + cause, + rendererIndex, + rendererFormat, + rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport); } /** @@ -107,7 +134,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForUnexpected(RuntimeException cause) { - return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_UNEXPECTED, cause); } /** @@ -127,14 +154,30 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) { - return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause); } - private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { + private ExoPlaybackException(@Type int type, Throwable cause) { + this( + type, + cause, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); + } + + private ExoPlaybackException( + @Type int type, + Throwable cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { super(cause); this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + this.rendererFormat = rendererFormat; + this.rendererFormatSupport = rendererFormatSupport; timestampMs = SystemClock.elapsedRealtime(); } @@ -142,6 +185,8 @@ public final class ExoPlaybackException extends Exception { super(message); this.type = type; rendererIndex = C.INDEX_UNSET; + rendererFormat = null; + rendererFormatSupport = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; cause = null; timestampMs = SystemClock.elapsedRealtime(); } 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 4c25c180f4..240c6436c4 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 @@ -378,7 +378,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { - Log.e(TAG, "Playback error.", e); + Log.e(TAG, getExoPlaybackExceptionMessage(e), e); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, @@ -411,6 +411,20 @@ import java.util.concurrent.atomic.AtomicBoolean; // Private methods. + private String getExoPlaybackExceptionMessage(ExoPlaybackException e) { + if (e.type != ExoPlaybackException.TYPE_RENDERER) { + return "Playback error."; + } + return "Renderer error: index=" + + e.rendererIndex + + ", type=" + + Util.getTrackTypeString(renderers[e.rendererIndex].getTrackType()) + + ", format=" + + e.rendererFormat + + ", rendererSupport=" + + RendererCapabilities.getFormatSupportString(e.rendererFormatSupport); + } + private void setState(int state) { if (playbackInfo.playbackState != state) { playbackInfo = playbackInfo.copyWithPlaybackState(state); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 95f1749f10..a75765262b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -237,6 +237,29 @@ public interface RendererCapabilities { return supportFlags & TUNNELING_SUPPORT_MASK; } + /** + * Returns string representation of a {@link FormatSupport} flag. + * + * @param formatSupport A {@link FormatSupport} flag. + * @return A string representation of the flag. + */ + static String getFormatSupportString(@FormatSupport int formatSupport) { + switch (formatSupport) { + case RendererCapabilities.FORMAT_HANDLED: + return "YES"; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + return "NO_UNSUPPORTED_TYPE"; + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + return "NO"; + default: + throw new IllegalStateException(); + } + } + /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a 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 3e48966c54..ae50d14728 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 @@ -90,10 +90,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsEosBufferTimestampWorkaround; private android.media.MediaFormat passthroughMediaFormat; - private @C.Encoding int pcmEncoding; - private int channelCount; - private int encoderDelay; - private int encoderPadding; + @Nullable private Format inputFormat; private long currentPositionUs; private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; @@ -551,15 +548,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { super.onInputFormatChanged(formatHolder); - Format newFormat = formatHolder.format; - eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - channelCount = newFormat.channelCount; - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; + inputFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(inputFormat); } @Override @@ -575,14 +565,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.getString(MediaFormat.KEY_MIME)); } else { mediaFormat = outputMediaFormat; - encoding = pcmEncoding; + encoding = getPcmEncoding(inputFormat); } int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; - if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { - channelMap = new int[this.channelCount]; - for (int i = 0; i < this.channelCount; i++) { + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && inputFormat.channelCount < 6) { + channelMap = new int[inputFormat.channelCount]; + for (int i = 0; i < inputFormat.channelCount; i++) { channelMap[i] = i; } } else { @@ -590,10 +580,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, - encoderPadding); + audioSink.configure( + encoding, + channelCount, + sampleRate, + 0, + channelMap, + inputFormat.encoderDelay, + inputFormat.encoderPadding); } catch (AudioSink.ConfigurationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @@ -820,7 +817,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } return false; } @@ -830,7 +828,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @@ -992,6 +991,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media || Util.DEVICE.startsWith("ms01")); } + @C.Encoding + private static int getPcmEncoding(Format format) { + // If the format is anything other than PCM then we assume that the audio decoder will output + // 16-bit PCM. + return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) + ? format.pcmEncoding + : C.ENCODING_PCM_16BIT; + } + private final class AudioSinkListener implements AudioSink.Listener { @Override 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 d5a5ffe7bb..5ccbf04c5c 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 @@ -263,7 +263,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return; } @@ -300,7 +300,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements TraceUtil.endSection(); } catch (AudioDecoderException | AudioSink.ConfigurationException | AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -483,7 +483,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -493,7 +493,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer. + throw createRendererException(e, inputFormat); } } @@ -644,7 +645,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements codecInitializedTimestamp - codecInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (AudioDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } 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 50b3ab8a0e..90b1d4286e 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 @@ -463,7 +463,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, format); } } @@ -538,7 +538,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } mediaCryptoRequiresSecureDecoder = !sessionMediaCrypto.forceAllowInsecureDecoderComponents @@ -548,7 +548,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) { @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { // Wait for keys. return; @@ -559,7 +559,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); } catch (DecoderInitializationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -722,8 +722,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { decoderCounters.ensureUpdated(); } catch (IllegalStateException e) { if (isMediaCodecException(e)) { - throw ExoPlaybackException.createForRenderer( - createDecoderException(e, getCodecInfo()), getIndex()); + throw createRendererException(e, inputFormat); } throw e; } @@ -1130,7 +1129,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { resetInputBuffer(); } } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return false; } @@ -1186,7 +1185,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; decoderCounters.inputBufferCount++; } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return true; } @@ -1199,7 +1198,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -1744,7 +1743,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } setCodecDrmSession(sourceDrmSession); codecDrainState = DRAIN_STATE_NONE; 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 d359eebfdb..058b1c4526 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 @@ -165,7 +165,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, streamFormat); } } @@ -247,7 +247,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, streamFormat); } } 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 6caf549afe..0a303c1df7 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 @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; -import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -218,7 +217,7 @@ public class EventLogger implements AnalyticsListener { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = - getFormatSupportString( + RendererCapabilities.getFormatSupportString( mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); logd( " " @@ -257,7 +256,8 @@ public class EventLogger implements AnalyticsListener { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); String formatSupport = - getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities.getFormatSupportString( + RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); logd( " " + status @@ -289,7 +289,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderEnabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderEnabled", Util.getTrackTypeString(trackType)); } @Override @@ -319,7 +319,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDecoderInitialized( EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { - logd(eventTime, "decoderInitialized", getTrackTypeString(trackType) + ", " + decoderName); + logd(eventTime, "decoderInitialized", Util.getTrackTypeString(trackType) + ", " + decoderName); } @Override @@ -327,12 +327,12 @@ public class EventLogger implements AnalyticsListener { logd( eventTime, "decoderInputFormat", - getTrackTypeString(trackType) + ", " + Format.toLogString(format)); + Util.getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @Override public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderDisabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderDisabled", Util.getTrackTypeString(trackType)); } @Override @@ -555,23 +555,6 @@ public class EventLogger implements AnalyticsListener { } } - private static String getFormatSupportString(@FormatSupport int formatSupport) { - switch (formatSupport) { - case RendererCapabilities.FORMAT_HANDLED: - return "YES"; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - return "NO_EXCEEDS_CAPABILITIES"; - case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: - return "NO_UNSUPPORTED_DRM"; - case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: - return "NO_UNSUPPORTED_TYPE"; - case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: - return "NO"; - default: - throw new IllegalStateException(); - } - } - private static String getAdaptiveSupportString( int trackCount, @AdaptiveSupport int adaptiveSupport) { if (trackCount < 2) { @@ -645,27 +628,6 @@ public class EventLogger implements AnalyticsListener { } } - private static String getTrackTypeString(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_AUDIO: - return "audio"; - case C.TRACK_TYPE_DEFAULT: - return "default"; - case C.TRACK_TYPE_METADATA: - return "metadata"; - case C.TRACK_TYPE_CAMERA_MOTION: - return "camera motion"; - case C.TRACK_TYPE_NONE: - return "none"; - case C.TRACK_TYPE_TEXT: - return "text"; - case C.TRACK_TYPE_VIDEO: - return "video"; - default: - return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; - } - } - private static String getPlaybackSuppressionReasonString( @PlaybackSuppressionReason int playbackSuppressionReason) { switch (playbackSuppressionReason) { 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 23447acddf..c8a947e7d6 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 @@ -2001,6 +2001,33 @@ public final class Util { return capabilities; } + /** + * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}. + * + * @param trackType A {@code TRACK_TYPE_*} constant, + * @return A string representation of this constant. + */ + public static String getTrackTypeString(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_DEFAULT: + return "default"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + case C.TRACK_TYPE_CAMERA_MOTION: + return "camera motion"; + case C.TRACK_TYPE_NONE: + return "none"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_VIDEO: + return "video"; + default: + return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; + } + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java index 9aa50e4388..bf0a28ffa0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -198,7 +198,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { while (feedInputBuffer()) {} TraceUtil.endSection(); } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -681,7 +681,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { decoderInitializedTimestamp - decoderInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -887,7 +887,7 @@ public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } From 4f363b1492345cc0ce00cb0d50ff0041f3b2c737 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 5 Dec 2019 18:19:13 +0000 Subject: [PATCH 795/807] Fix mdta handling on I/O error An I/O error could occur while handling the start of an mdta box, in which case retrying would cause another ContainerAtom to be added. Fix this by skipping the mdta header before updating container atoms. PiperOrigin-RevId: 284000715 --- .../android/exoplayer2/extractor/mp4/Mp4Extractor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 16f5b1fb29..ad58e832aa 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 @@ -304,13 +304,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { - if (atomType == Atom.TYPE_meta) { - maybeSkipRemainingMetaAtomHeaderBytes(input); - } // Start reading the first child atom. enterReadingAtomHeaderState(); } From 5973b76481392f5f84fedb1603ad7440f2240bd2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 5 Dec 2019 18:20:07 +0000 Subject: [PATCH 796/807] MatroskaExtractor naming cleanup - Change sampleHasReferenceBlock to a block reading variable, which is what it is (the distinction didn't matter previously, but will do so when we add lacing support in full blocks because there wont be a 1:1 relationship any more. - Move sampleRead to be a reading state variable. - Stop abbreviating "additional" Issue: #3026 PiperOrigin-RevId: 284000937 --- .../extractor/mkv/MatroskaExtractor.java | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 69bdb2cd46..ff64357ca7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -224,7 +224,7 @@ public class MatroskaExtractor implements Extractor { * BlockAddID value for ITU T.35 metadata in a VP9 track. See also * https://www.webmproject.org/docs/container/. */ - private static final int BLOCK_ADD_ID_VP9_ITU_T_35 = 4; + private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; @@ -332,7 +332,7 @@ public class MatroskaExtractor implements Extractor { private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; - private final ParsableByteArray blockAddData; + private final ParsableByteArray blockAdditionalData; private ByteBuffer encryptionSubsampleDataBuffer; private long segmentContentSize; @@ -360,6 +360,9 @@ public class MatroskaExtractor implements Extractor { private LongArray cueClusterPositions; private boolean seenClusterPositionForCurrentCuePoint; + // Reading state. + private boolean haveOutputSample; + // Block reading state. private int blockState; private long blockTimeUs; @@ -371,20 +374,19 @@ public class MatroskaExtractor implements Extractor { private int blockTrackNumberLength; @C.BufferFlags private int blockFlags; - private int blockAddId; + private int blockAdditionalId; + private boolean blockHasReferenceBlock; // Sample reading state. private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; private boolean sampleEncodingHandled; private boolean sampleSignalByteRead; - private boolean sampleInitializationVectorRead; private boolean samplePartitionCountRead; - private byte sampleSignalByte; private int samplePartitionCount; - private int sampleCurrentNalBytesRemaining; - private int sampleBytesWritten; - private boolean sampleRead; - private boolean sampleSeenReferenceBlock; + private byte sampleSignalByte; + private boolean sampleInitializationVectorRead; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -412,7 +414,7 @@ public class MatroskaExtractor implements Extractor { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); - blockAddData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); } @Override @@ -446,9 +448,9 @@ public class MatroskaExtractor implements Extractor { @Override public final int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - sampleRead = false; + haveOutputSample = false; boolean continueReading = true; - while (continueReading && !sampleRead) { + while (continueReading && !haveOutputSample) { continueReading = reader.read(input); if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { return Extractor.RESULT_SEEK; @@ -623,7 +625,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_GROUP: - sampleSeenReferenceBlock = false; + blockHasReferenceBlock = false; break; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. @@ -681,7 +683,7 @@ public class MatroskaExtractor implements Extractor { return; } // If the ReferenceBlock element was not found for this sample, then it is a keyframe. - if (!sampleSeenReferenceBlock) { + if (!blockHasReferenceBlock) { blockFlags |= C.BUFFER_FLAG_KEY_FRAME; } commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); @@ -793,7 +795,7 @@ public class MatroskaExtractor implements Extractor { currentTrack.audioBitDepth = (int) value; break; case ID_REFERENCE_BLOCK: - sampleSeenReferenceBlock = true; + blockHasReferenceBlock = true; break; case ID_CONTENT_ENCODING_ORDER: // This extractor only supports one ContentEncoding element and hence the order has to be 0. @@ -935,7 +937,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_ADD_ID: - blockAddId = (int) value; + blockAdditionalId = (int) value; break; default: break; @@ -1199,7 +1201,8 @@ public class MatroskaExtractor implements Extractor { if (blockState != BLOCK_STATE_DATA) { return; } - handleBlockAdditionalData(tracks.get(blockTrackNumber), blockAddId, input, contentSize); + handleBlockAdditionalData( + tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize); break; default: throw new ParserException("Unexpected id: " + id); @@ -1207,11 +1210,12 @@ public class MatroskaExtractor implements Extractor { } protected void handleBlockAdditionalData( - Track track, int blockAddId, ExtractorInput input, int contentSize) + Track track, int blockAdditionalId, ExtractorInput input, int contentSize) throws IOException, InterruptedException { - if (blockAddId == BLOCK_ADD_ID_VP9_ITU_T_35 && CODEC_ID_VP9.equals(track.codecId)) { - blockAddData.reset(contentSize); - input.readFully(blockAddData.data, 0, contentSize); + if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 + && CODEC_ID_VP9.equals(track.codecId)) { + blockAdditionalData.reset(contentSize); + input.readFully(blockAdditionalData.data, 0, contentSize); } else { // Unhandled block additional data. input.skipFully(contentSize); @@ -1236,13 +1240,13 @@ public class MatroskaExtractor implements Extractor { if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { // Append supplemental data. - int size = blockAddData.limit(); - track.output.sampleData(blockAddData, size); - sampleBytesWritten += size; + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + sampleBytesWritten += blockAdditionalSize; } track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); } - sampleRead = true; + haveOutputSample = true; resetSample(); } @@ -1375,7 +1379,7 @@ public class MatroskaExtractor implements Extractor { if (track.maxBlockAdditionId > 0) { blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; - blockAddData.reset(); + blockAdditionalData.reset(); // If there is supplemental data, the structure of the sample data is: // sample size (4 bytes) || sample data || supplemental data scratch.reset(/* limit= */ 4); From 22f25c57bbbb63fd0e7ee1ece52e455f5e534b9f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 6 Dec 2019 11:09:44 +0000 Subject: [PATCH 797/807] MatroskaExtractor naming cleanup II - Remove "lacing" from member variables. They're used even if there is no lacing (and the fact that lacing is the way of getting multiple samples into a block isn't important). Issue: #3026 PiperOrigin-RevId: 284152447 --- .../extractor/mkv/MatroskaExtractor.java | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ff64357ca7..31f9f32484 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -367,9 +367,9 @@ public class MatroskaExtractor implements Extractor { private int blockState; private long blockTimeUs; private long blockDurationUs; - private int blockLacingSampleIndex; - private int blockLacingSampleCount; - private int[] blockLacingSampleSizes; + private int blockSampleIndex; + private int blockSampleCount; + private int[] blockSampleSizes; private int blockTrackNumber; private int blockTrackNumberLength; @C.BufferFlags @@ -1093,9 +1093,9 @@ public class MatroskaExtractor implements Extractor { readScratch(input, 3); int lacing = (scratch.data[2] & 0x06) >> 1; if (lacing == LACING_NONE) { - blockLacingSampleCount = 1; - blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1); - blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3; + blockSampleCount = 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); + blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; } else { if (id != ID_SIMPLE_BLOCK) { throw new ParserException("Lacing only supported in SimpleBlocks."); @@ -1103,33 +1103,32 @@ public class MatroskaExtractor implements Extractor { // Read the sample count (1 byte). readScratch(input, 4); - blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1; - blockLacingSampleSizes = - ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount); + blockSampleCount = (scratch.data[3] & 0xFF) + 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount); if (lacing == LACING_FIXED_SIZE) { int blockLacingSampleSize = - (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount; - Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize); + (contentSize - blockTrackNumberLength - 4) / blockSampleCount; + Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize); } else if (lacing == LACING_XIPH) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; int byteValue; do { readScratch(input, ++headerSize); byteValue = scratch.data[headerSize - 1] & 0xFF; - blockLacingSampleSizes[sampleIndex] += byteValue; + blockSampleSizes[sampleIndex] += byteValue; } while (byteValue == 0xFF); - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else if (lacing == LACING_EBML) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; readScratch(input, ++headerSize); if (scratch.data[headerSize - 1] == 0) { throw new ParserException("No valid varint length mask found"); @@ -1157,11 +1156,13 @@ public class MatroskaExtractor implements Extractor { throw new ParserException("EBML lacing sample size out of range."); } int intReadValue = (int) readValue; - blockLacingSampleSizes[sampleIndex] = sampleIndex == 0 - ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue; - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + blockSampleSizes[sampleIndex] = + sampleIndex == 0 + ? intReadValue + : blockSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else { // Lacing is always in the range 0--3. @@ -1177,23 +1178,23 @@ public class MatroskaExtractor implements Extractor { blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0) | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0); blockState = BLOCK_STATE_DATA; - blockLacingSampleIndex = 0; + blockSampleIndex = 0; } if (id == ID_SIMPLE_BLOCK) { // For SimpleBlock, we have metadata for each sample here. - while (blockLacingSampleIndex < blockLacingSampleCount) { - writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]); - long sampleTimeUs = blockTimeUs - + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; + while (blockSampleIndex < blockSampleCount) { + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + long sampleTimeUs = + blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; commitSampleToOutput(track, sampleTimeUs); - blockLacingSampleIndex++; + blockSampleIndex++; } blockState = BLOCK_STATE_START; } else { // For Block, we send the metadata at the end of the BlockGroup element since we'll know // if the sample is a keyframe or not only at that point. - writeSampleData(input, track, blockLacingSampleSizes[0]); + writeSampleData(input, track, blockSampleSizes[0]); } break; From 7e93c5c0b6435fa185df9b38e1831dad2a92ed7b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 6 Dec 2019 11:33:10 +0000 Subject: [PATCH 798/807] Enable physical display size hacks for API level 29 For AOSP TV devices that might not pass manual verification. PiperOrigin-RevId: 284154763 --- .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c8a947e7d6..0ee52dba2b 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 @@ -1927,7 +1927,7 @@ public final class Util { * @return The physical display size, in pixels. */ public static Point getPhysicalDisplaySize(Context context, Display display) { - if (Util.SDK_INT <= 28 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { + if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { // On Android TVs it is common for the UI to be configured for a lower resolution than // SurfaceViews can output. Before API 26 the Display object does not provide a way to // identify this case, and up to and including API 28 many devices still do not correctly set From bdcdabac0142c0541d74f35361c2f34c35811398 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 6 Dec 2019 23:32:04 +0000 Subject: [PATCH 799/807] Finalize release notes --- RELEASENOTES.md | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c9564e2d58..83ccb1be16 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes # -### 2.11.0 (not yet released) ### +### 2.11.0 (2019-12-11) ### * Core library: * Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and @@ -82,14 +82,14 @@ ([#5782](https://github.com/google/ExoPlayer/issues/5782)). * Reconfigure audio sink when PCM encoding changes ([#6601](https://github.com/google/ExoPlayer/issues/6601)). - * Allow `AdtsExtractor` to encounter EoF when calculating average frame size + * Allow `AdtsExtractor` to encounter EOF when calculating average frame size ([#6700](https://github.com/google/ExoPlayer/issues/6700)). * Text: + * Add support for position and overlapping start/end times in SSA/ASS + subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)). * Require an end time or duration for SubRip (SRT) and SubStation Alpha (SSA/ASS) subtitles. This applies to both sidecar files & subtitles [embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html). - * Reconfigure audio sink when PCM encoding changes - ([#6601](https://github.com/google/ExoPlayer/issues/6601)). * UI: * Make showing and hiding player controls accessible to TalkBack in `PlayerView`. @@ -101,7 +101,7 @@ * Remove `AnalyticsCollector.Factory`. Instances should be created directly, and the `Player` should be set by calling `AnalyticsCollector.setPlayer`. * Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and - analytics reporting (TODO: link to developer guide page/blog post). + analytics reporting. * DataSource * Add `DataSpec.httpRequestHeaders` to support setting per-request headers for HTTP and HTTPS. @@ -130,30 +130,27 @@ `C.MSG_SET_OUTPUT_BUFFER_RENDERER`. * Use `VideoDecoderRenderer` as an implementation of `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. -* Flac extension: - * Update to use NDK r20. - * Fix build - ([#6601](https://github.com/google/ExoPlayer/issues/6601). +* Flac extension: Update to use NDK r20. +* Opus extension: Update to use NDK r20. * FFmpeg extension: * Update to use NDK r20. * Update to use FFmpeg version 4.2. It is necessary to rebuild the native part of the extension after this change, following the instructions in the extension's readme. -* Opus extension: Update to use NDK r20. -* MediaSession extension: Make media session connector dispatch - `ACTION_SET_CAPTIONING_ENABLED`. +* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to + support `ACTION_SET_CAPTIONING_ENABLED` events. * GVR extension: This extension is now deprecated. -* Demo apps (TODO: update links to point to r2.11.0 tag): - * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/surface) +* Demo apps: + * Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface) to show how to use the Android 10 `SurfaceControl` API with ExoPlayer ([#677](https://github.com/google/ExoPlayer/issues/677)). * Add support for subtitle files to the - [Main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main) + [Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main) ([#5523](https://github.com/google/ExoPlayer/issues/5523)). * Remove the IMA demo app. IMA functionality is demonstrated by the - [main demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/main). + [main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main). * Add basic DRM support to the - [Cast demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/cast). + [Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast). * TestUtils: Publish the `testutils` module to simplify unit testing with ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)). * IMA extension: Remove `AdsManager` listeners on release to avoid leaking an From 567f2a6575a9c4e92762be9d411fbe49b902ac80 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 09:56:50 +0000 Subject: [PATCH 800/807] Fix Javadoc issues PiperOrigin-RevId: 284509437 --- .../google/android/exoplayer2/extractor/ExtractorInput.java | 6 +++--- .../com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java index 1b492e38c7..461b059bad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -33,7 +33,7 @@ import java.io.InputStream; * wants to read an entire block/frame/header of known length. * * - *

        {@link InputStream}-like methods

        + *

        {@link InputStream}-like methods

        * *

        The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level * access operations. The {@code length} parameter is a maximum, and each method returns the number @@ -41,7 +41,7 @@ import java.io.InputStream; * was reached, or the method was interrupted, or the operation was aborted early for another * reason. * - *

        Block-based methods

        + *

        Block-based methods

        * *

        The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user * wants to read an entire block/frame/header of known length. @@ -218,7 +218,7 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int,)} + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)} * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. 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 45d4554bb7..917ac8e36e 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 @@ -72,7 +72,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } /** - * Constructs an SsaDecoder with optional format & header info. + * Constructs an SsaDecoder with optional format and header info. * * @param initializationData Optional initialization data for the decoder. If not null or empty, * the initialization data must consist of two byte arrays. The first must contain an SSA From 0065f63f480028983ad98c8b8fbce30d766d77ed Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 14:34:12 +0000 Subject: [PATCH 801/807] MatroskaExtractor: Constrain use of sample state member variables This change constrains the use of sample state member variables to writeSampleData, finishWriteSampleData and resetWriteSampleData. Using them elsewhere gets increasingly confusing when considering features like lacing in full blocks. For example sampleBytesWritten cannot be used when calling commitSampleToOutput in this case because we need to write the sample data for multiple samples before we commit any of them. Issue: #3026 PiperOrigin-RevId: 284541942 --- .../extractor/mkv/MatroskaExtractor.java | 189 ++++++++++-------- 1 file changed, 110 insertions(+), 79 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 31f9f32484..0b7b5bd053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -377,7 +377,7 @@ public class MatroskaExtractor implements Extractor { private int blockAdditionalId; private boolean blockHasReferenceBlock; - // Sample reading state. + // Sample writing state. private int sampleBytesRead; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; @@ -434,7 +434,7 @@ public class MatroskaExtractor implements Extractor { blockState = BLOCK_STATE_START; reader.reset(); varintReader.reset(); - resetSample(); + resetWriteSampleData(); for (int i = 0; i < tracks.size(); i++) { tracks.valueAt(i).reset(); } @@ -686,7 +686,12 @@ public class MatroskaExtractor implements Extractor { if (!blockHasReferenceBlock) { blockFlags |= C.BUFFER_FLAG_KEY_FRAME; } - commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); + commitSampleToOutput( + tracks.get(blockTrackNumber), + blockTimeUs, + blockFlags, + blockSampleSizes[0], + /* offset= */ 0); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: @@ -1184,17 +1189,17 @@ public class MatroskaExtractor implements Extractor { if (id == ID_SIMPLE_BLOCK) { // For SimpleBlock, we have metadata for each sample here. while (blockSampleIndex < blockSampleCount) { - writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); long sampleTimeUs = blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; - commitSampleToOutput(track, sampleTimeUs); + commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0); blockSampleIndex++; } blockState = BLOCK_STATE_START; } else { // For Block, we send the metadata at the end of the BlockGroup element since we'll know // if the sample is a keyframe or not only at that point. - writeSampleData(input, track, blockSampleSizes[0]); + blockSampleSizes[0] = writeSampleData(input, track, blockSampleSizes[0]); } break; @@ -1223,9 +1228,10 @@ public class MatroskaExtractor implements Extractor { } } - private void commitSampleToOutput(Track track, long timeUs) { + private void commitSampleToOutput( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { if (track.trueHdSampleRechunker != null) { - track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); } else { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { if (durationUs == C.TIME_UNSET) { @@ -1235,33 +1241,19 @@ public class MatroskaExtractor implements Extractor { // Note: If we ever want to support DRM protected subtitles then we'll need to output the // appropriate encryption data here. track.output.sampleData(subtitleSample, subtitleSample.limit()); - sampleBytesWritten += subtitleSample.limit(); + size += subtitleSample.limit(); } } - if ((blockFlags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { // Append supplemental data. int blockAdditionalSize = blockAdditionalData.limit(); track.output.sampleData(blockAdditionalData, blockAdditionalSize); - sampleBytesWritten += blockAdditionalSize; + size += blockAdditionalSize; } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); + track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); } haveOutputSample = true; - resetSample(); - } - - private void resetSample() { - sampleBytesRead = 0; - sampleBytesWritten = 0; - sampleCurrentNalBytesRemaining = 0; - sampleEncodingHandled = false; - sampleSignalByteRead = false; - samplePartitionCountRead = false; - samplePartitionCount = 0; - sampleSignalByte = (byte) 0; - sampleInitializationVectorRead = false; - sampleStrippedBytes.reset(); } /** @@ -1281,14 +1273,24 @@ public class MatroskaExtractor implements Extractor { scratch.setLimit(requiredLength); } - private void writeSampleData(ExtractorInput input, Track track, int size) + /** + * Writes data for a single sample to the track output. + * + * @param input The input from which to read sample data. + * @param track The track to output the sample to. + * @param size The size of the sample data on the input side. + * @return The final size of the written sample. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int writeSampleData(ExtractorInput input, Track track, int size) throws IOException, InterruptedException { if (CODEC_ID_SUBRIP.equals(track.codecId)) { writeSubtitleSampleData(input, SUBRIP_PREFIX, size); - return; + return finishWriteSampleData(); } else if (CODEC_ID_ASS.equals(track.codecId)) { writeSubtitleSampleData(input, SSA_PREFIX, size); - return; + return finishWriteSampleData(); } TrackOutput output = track.output; @@ -1413,8 +1415,9 @@ public class MatroskaExtractor implements Extractor { while (sampleBytesRead < size) { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. - readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff, - nalUnitLengthFieldLength); + writeToTarget( + input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); // Write a start code for the current NAL unit. @@ -1423,17 +1426,21 @@ public class MatroskaExtractor implements Extractor { sampleBytesWritten += 4; } else { // Write the payload of the NAL unit. - sampleCurrentNalBytesRemaining -= - readToOutput(input, output, sampleCurrentNalBytesRemaining); + int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + sampleCurrentNalBytesRemaining -= bytesWritten; } } } else { if (track.trueHdSampleRechunker != null) { Assertions.checkState(sampleStrippedBytes.limit() == 0); - track.trueHdSampleRechunker.startSample(input, blockFlags, size); + track.trueHdSampleRechunker.startSample(input); } while (sampleBytesRead < size) { - readToOutput(input, output, size - sampleBytesRead); + int bytesWritten = writeToOutput(input, output, size - sampleBytesRead); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; } } @@ -1448,6 +1455,32 @@ public class MatroskaExtractor implements Extractor { output.sampleData(vorbisNumPageSamples, 4); sampleBytesWritten += 4; } + + return finishWriteSampleData(); + } + + /** + * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been + * written. Returns the final sample size and resets state for the next sample. + */ + private int finishWriteSampleData() { + int sampleSize = sampleBytesWritten; + resetWriteSampleData(); + return sampleSize; + } + + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + private void resetWriteSampleData() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleSignalByteRead = false; + samplePartitionCountRead = false; + samplePartitionCount = 0; + sampleSignalByte = (byte) 0; + sampleInitializationVectorRead = false; + sampleStrippedBytes.reset(); } private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) @@ -1515,8 +1548,9 @@ public class MatroskaExtractor implements Extractor { int seconds = (int) (timeUs / C.MICROS_PER_SECOND); timeUs -= (seconds * C.MICROS_PER_SECOND); int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); - timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, - seconds, lastValue)); + timeCodeData = + Util.getUtf8Bytes( + String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); return timeCodeData; } @@ -1524,33 +1558,30 @@ public class MatroskaExtractor implements Extractor { * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}. */ - private void readToTarget(ExtractorInput input, byte[] target, int offset, int length) + private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length) throws IOException, InterruptedException { int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft()); input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes); if (pendingStrippedBytes > 0) { sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes); } - sampleBytesRead += length; } /** * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either * {@link #sampleStrippedBytes} or data read from {@code input}. */ - private int readToOutput(ExtractorInput input, TrackOutput output, int length) + private int writeToOutput(ExtractorInput input, TrackOutput output, int length) throws IOException, InterruptedException { - int bytesRead; + int bytesWritten; int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); if (strippedBytesLeft > 0) { - bytesRead = Math.min(length, strippedBytesLeft); - output.sampleData(sampleStrippedBytes, bytesRead); + bytesWritten = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesWritten); } else { - bytesRead = output.sampleData(input, length, false); + bytesWritten = output.sampleData(input, length, false); } - sampleBytesRead += bytesRead; - sampleBytesWritten += bytesRead; - return bytesRead; + return bytesWritten; } /** @@ -1725,10 +1756,11 @@ public class MatroskaExtractor implements Extractor { private final byte[] syncframePrefix; private boolean foundSyncframe; - private int sampleCount; + private int chunkSampleCount; + private long chunkTimeUs; + private @C.BufferFlags int chunkFlags; private int chunkSize; - private long timeUs; - private @C.BufferFlags int blockFlags; + private int chunkOffset; public TrueHdSampleRechunker() { syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; @@ -1736,47 +1768,46 @@ public class MatroskaExtractor implements Extractor { public void reset() { foundSyncframe = false; + chunkSampleCount = 0; } - public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) - throws IOException, InterruptedException { - if (!foundSyncframe) { - input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); - input.resetPeekPosition(); - if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { - return; - } - foundSyncframe = true; - sampleCount = 0; + public void startSample(ExtractorInput input) throws IOException, InterruptedException { + if (foundSyncframe) { + return; } - if (sampleCount == 0) { - // This is the first sample in the chunk, so reset the block flags and chunk size. - this.blockFlags = blockFlags; + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; + } + foundSyncframe = true; + } + + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (!foundSyncframe) { + return; + } + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; chunkSize = 0; } chunkSize += size; - } - - public void sampleMetadata(Track track, long timeUs) { - if (!foundSyncframe) { - return; - } - if (sampleCount++ == 0) { - // This is the first sample in the chunk, so update the timestamp. - this.timeUs = timeUs; - } - if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + chunkOffset = offset; // The offset is to the end of the sample. + if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { // We haven't read enough samples to output a chunk. return; } - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; + outputPendingSampleMetadata(track); } public void outputPendingSampleMetadata(Track track) { - if (foundSyncframe && sampleCount > 0) { - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; + if (chunkSampleCount > 0) { + track.output.sampleMetadata( + chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData); + chunkSampleCount = 0; } } } From 914a8df0adfa02bcdd9eb1f7e4f596e28ec205a0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Dec 2019 15:02:11 +0000 Subject: [PATCH 802/807] MatroskaExtractor: Support lacing in full blocks Caveats: - Block additional data is ignored if the block is laced and contains multiple samples. Note that this is not a loss of functionality (SimpleBlock cannot have block additional data, and lacing was previously completely unsupported for Block) - Subrip and ASS samples are dropped if they're in laced blocks with multiple samples (I don't think this is valid anyway) Issue: #3026 PiperOrigin-RevId: 284545197 --- RELEASENOTES.md | 2 + .../extractor/mkv/MatroskaExtractor.java | 64 ++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 83ccb1be16..19a7868727 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -117,6 +117,8 @@ * Fix issue where streams could get stuck in an infinite buffering state after a postroll ad ([#6314](https://github.com/google/ExoPlayer/issues/6314)). +* Matroska: Support lacing in Blocks + ([#3026](https://github.com/google/ExoPlayer/issues/3026)). * AV1 extension: * New in this release. The AV1 extension allows use of the [libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 0b7b5bd053..403f6c3d41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -682,16 +682,24 @@ public class MatroskaExtractor implements Extractor { // We've skipped this block (due to incompatible track number). return; } - // If the ReferenceBlock element was not found for this sample, then it is a keyframe. - if (!blockHasReferenceBlock) { - blockFlags |= C.BUFFER_FLAG_KEY_FRAME; + // Commit sample metadata. + int sampleOffset = 0; + for (int i = 0; i < blockSampleCount; i++) { + sampleOffset += blockSampleSizes[i]; + } + Track track = tracks.get(blockTrackNumber); + for (int i = 0; i < blockSampleCount; i++) { + long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000; + int sampleFlags = blockFlags; + if (i == 0 && !blockHasReferenceBlock) { + // If the ReferenceBlock element was not found in this block, then the first frame is a + // keyframe. + sampleFlags |= C.BUFFER_FLAG_KEY_FRAME; + } + int sampleSize = blockSampleSizes[i]; + sampleOffset -= sampleSize; // The offset is to the end of the sample. + commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset); } - commitSampleToOutput( - tracks.get(blockTrackNumber), - blockTimeUs, - blockFlags, - blockSampleSizes[0], - /* offset= */ 0); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: @@ -1102,10 +1110,6 @@ public class MatroskaExtractor implements Extractor { blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; } else { - if (id != ID_SIMPLE_BLOCK) { - throw new ParserException("Lacing only supported in SimpleBlocks."); - } - // Read the sample count (1 byte). readScratch(input, 4); blockSampleCount = (scratch.data[3] & 0xFF) + 1; @@ -1187,7 +1191,8 @@ public class MatroskaExtractor implements Extractor { } if (id == ID_SIMPLE_BLOCK) { - // For SimpleBlock, we have metadata for each sample here. + // For SimpleBlock, we can write sample data and immediately commit the corresponding + // sample metadata. while (blockSampleIndex < blockSampleCount) { int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); long sampleTimeUs = @@ -1197,9 +1202,16 @@ public class MatroskaExtractor implements Extractor { } blockState = BLOCK_STATE_START; } else { - // For Block, we send the metadata at the end of the BlockGroup element since we'll know - // if the sample is a keyframe or not only at that point. - blockSampleSizes[0] = writeSampleData(input, track, blockSampleSizes[0]); + // For Block, we need to wait until the end of the BlockGroup element before committing + // sample metadata. This is so that we can handle ReferenceBlock (which can be used to + // infer whether the first sample in the block is a keyframe), and BlockAdditions (which + // can contain additional sample data to append) contained in the block group. Just output + // the sample data, storing the final sample sizes for when we commit the metadata. + while (blockSampleIndex < blockSampleCount) { + blockSampleSizes[blockSampleIndex] = + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + blockSampleIndex++; + } } break; @@ -1234,7 +1246,9 @@ public class MatroskaExtractor implements Extractor { track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); } else { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { - if (durationUs == C.TIME_UNSET) { + if (blockSampleCount > 1) { + Log.w(TAG, "Skipping subtitle sample in laced block."); + } else if (durationUs == C.TIME_UNSET) { Log.w(TAG, "Skipping subtitle sample with no duration."); } else { setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); @@ -1246,10 +1260,16 @@ public class MatroskaExtractor implements Extractor { } if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { - // Append supplemental data. - int blockAdditionalSize = blockAdditionalData.limit(); - track.output.sampleData(blockAdditionalData, blockAdditionalSize); - size += blockAdditionalSize; + if (blockSampleCount > 1) { + // There were multiple samples in the block. Appending the additional data to the last + // sample doesn't make sense. Skip instead. + flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + } else { + // Append supplemental data. + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + size += blockAdditionalSize; + } } track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); } From 1de7ec2c703de7b1d657507b497f1a9c488e61da Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Dec 2019 12:33:47 +0000 Subject: [PATCH 803/807] Fix bug removing entries from CacheFileMetadataIndex Issue: #6621 PiperOrigin-RevId: 284743414 --- .../cache/CacheFileMetadataIndex.java | 2 +- .../cache/CacheFileMetadataIndexTest.java | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java 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 dc27dec363..e288a5258e 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 @@ -43,7 +43,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private static final int COLUMN_INDEX_LENGTH = 1; private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; - private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; + private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?"; private static final String[] COLUMNS = new String[] { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java new file mode 100644 index 0000000000..283487f7ea --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndexTest.java @@ -0,0 +1,135 @@ +/* + * 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.cache; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.database.DatabaseIOException; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.util.HashSet; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests {@link CacheFileMetadataIndex}. */ +@RunWith(AndroidJUnit4.class) +public class CacheFileMetadataIndexTest { + + @Test + public void initiallyEmpty() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + assertThat(index.getAll()).isEmpty(); + } + + @Test + public void insert() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(2); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(123); + assertThat(metadata.lastTouchTimestamp).isEqualTo(456); + + metadata = all.get("name2"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + + metadata = all.get("name3"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndRemove() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + index.remove("name1"); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(1); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNull(); + + metadata = all.get("name2"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + + index.remove("name2"); + + all = index.getAll(); + assertThat(all).isEmpty(); + + metadata = all.get("name2"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndRemoveAll() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name2", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + HashSet namesToRemove = new HashSet<>(); + namesToRemove.add("name1"); + namesToRemove.add("name2"); + index.removeAll(namesToRemove); + + Map all = index.getAll(); + assertThat(all.isEmpty()).isTrue(); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNull(); + + metadata = all.get("name2"); + assertThat(metadata).isNull(); + } + + @Test + public void insertAndReplace() throws DatabaseIOException { + CacheFileMetadataIndex index = newInitializedIndex(); + + index.set("name1", /* length= */ 123, /* lastTouchTimestamp= */ 456); + index.set("name1", /* length= */ 789, /* lastTouchTimestamp= */ 123); + + Map all = index.getAll(); + assertThat(all.size()).isEqualTo(1); + + CacheFileMetadata metadata = all.get("name1"); + assertThat(metadata).isNotNull(); + assertThat(metadata.length).isEqualTo(789); + assertThat(metadata.lastTouchTimestamp).isEqualTo(123); + } + + private static CacheFileMetadataIndex newInitializedIndex() throws DatabaseIOException { + CacheFileMetadataIndex index = + new CacheFileMetadataIndex(TestUtil.getInMemoryDatabaseProvider()); + index.initialize(/* uid= */ 1234); + return index; + } +} From b8eafea176b31a3ead1697093d6c474e9fa9d692 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 10 Dec 2019 18:04:53 +0000 Subject: [PATCH 804/807] Add missing release note entry PiperOrigin-RevId: 284792946 --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 19a7868727..67a5ba083d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -423,6 +423,7 @@ * Update `TrackSelection.Factory` interface to support creating all track selections together. * Allow to specify a selection reason for a `SelectionOverride`. + * Select audio track based on system language if no preference is provided. * When no text language preference matches, only select forced text tracks whose language matches the selected audio language. * UI: From 03b02f98df13f6e67b2d9061f3502f8f2c3f25d1 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Tue, 10 Dec 2019 18:29:59 +0000 Subject: [PATCH 805/807] Fix an issue where a keyframe was not skipped. Keyframe was rendered rather than skipped when performing an exact seek to a non-zero position close to the start of the stream. PiperOrigin-RevId: 284798460 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 67a5ba083d..5add56ca08 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -75,6 +75,9 @@ * Fix Dolby Vision fallback to AVC and HEVC. * Fix early end-of-stream detection when using video tunneling, on API level 23 and above. + * Fix an issue where a keyframe was rendered rather than skipped when + performing an exact seek to a non-zero position close to the start of the + stream. * Audio: * Fix the start of audio getting truncated when transitioning to a new item in a playlist of Opus streams. 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 90b1d4286e..820f9f003e 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 @@ -365,8 +365,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - private long lastBufferInStreamPresentationTimeUs; private long largestQueuedPresentationTimeUs; + private long lastBufferInStreamPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -954,6 +954,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReceivedEos = false; codecReceivedBuffers = false; + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; From 6ebc9f96c817d25d6b47af106924df3751905089 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 10 Dec 2019 15:05:13 +0000 Subject: [PATCH 806/807] Fix generics warning in FakeAdaptiveMediaPeriod. Remove all generic arrays from this class. FakeAdaptiveMediaPeriod.java:171: warning: [rawtypes] found raw type: ChunkSampleStream return new ChunkSampleStream[length]; ^ missing type arguments for generic class ChunkSampleStream where T is a type-variable: T extends ChunkSource declared in class ChunkSampleStream PiperOrigin-RevId: 284761750 --- .../testutil/FakeAdaptiveMediaPeriod.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 54b5baea57..26d29d71f6 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 @@ -45,7 +45,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final long durationUs; private Callback callback; - private ChunkSampleStream[] sampleStreams; + private List> sampleStreams; private SequenceableLoader sequenceableLoader; public FakeAdaptiveMediaPeriod( @@ -60,7 +60,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.durationUs = durationUs; - this.sampleStreams = newSampleStreamArray(0); + this.sampleStreams = new ArrayList<>(); this.sequenceableLoader = new CompositeSequenceableLoader(new SequenceableLoader[0]); } @@ -94,8 +94,9 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod validStreams.add((ChunkSampleStream) stream); } } - this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size())); - this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + this.sampleStreams = validStreams; + this.sequenceableLoader = + new CompositeSequenceableLoader(sampleStreams.toArray(new SequenceableLoader[0])); return returnPositionUs; } @@ -165,9 +166,4 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod public void onContinueLoadingRequested(ChunkSampleStream source) { callback.onContinueLoadingRequested(this); } - - @SuppressWarnings("unchecked") - private static ChunkSampleStream[] newSampleStreamArray(int length) { - return new ChunkSampleStream[length]; - } } From a4a9cc9fd0044b6e6ebd051d0d645c3176ae3472 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 11 Dec 2019 13:43:31 +0000 Subject: [PATCH 807/807] Suppress rawtypes warning when instantiating generic array Change FakeAdaptiveMediaPeriod back to this style for consistency. PiperOrigin-RevId: 284967667 --- .../exoplayer2/source/dash/DashMediaPeriod.java | 3 ++- .../source/smoothstreaming/SsMediaPeriod.java | 3 ++- .../testutil/FakeAdaptiveMediaPeriod.java | 15 ++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) 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 bb8226e172..88de84603e 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 @@ -818,7 +818,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* initializationData= */ null); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } 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 42ac82e553..f7940fed1b 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 @@ -277,7 +277,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return new TrackGroupArray(trackGroups); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } 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 26d29d71f6..011270d543 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 @@ -45,7 +45,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final long durationUs; private Callback callback; - private List> sampleStreams; + private ChunkSampleStream[] sampleStreams; private SequenceableLoader sequenceableLoader; public FakeAdaptiveMediaPeriod( @@ -60,7 +60,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.durationUs = durationUs; - this.sampleStreams = new ArrayList<>(); + this.sampleStreams = newSampleStreamArray(0); this.sequenceableLoader = new CompositeSequenceableLoader(new SequenceableLoader[0]); } @@ -94,9 +94,8 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod validStreams.add((ChunkSampleStream) stream); } } - this.sampleStreams = validStreams; - this.sequenceableLoader = - new CompositeSequenceableLoader(sampleStreams.toArray(new SequenceableLoader[0])); + this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size())); + this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return returnPositionUs; } @@ -166,4 +165,10 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod public void onContinueLoadingRequested(ChunkSampleStream source) { callback.onContinueLoadingRequested(this); } + + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) + private static ChunkSampleStream[] newSampleStreamArray(int length) { + return new ChunkSampleStream[length]; + } }