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 4d90fa962a9..38889e1a21a 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 00000000000..61288681e8e --- /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 00000000000..8580638896c --- /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 00000000000..f6fa0df5aa5 --- /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 00000000000..d125b767961 --- /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 d125b767961..0b85cc15465 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 boolean schedule(Requirements requirements, String servicePackage, String 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 f8326dd503e..6ae3ccf2e6c 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 85cf87aa85e..e82cf32552d 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 61288681e8e..e1d122b84b6 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 8580638896c..1d017e4ff2f 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 0b85cc15465..4d925604651 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.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 1d017e4ff2f..8e2c8fc4235 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 4d925604651..0df75c495ba 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 @@ private static Constraints buildConstraints(Requirements requirements) { 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 8e2c8fc4235..41145c0d20e 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 7089d4d7314..494af011a55 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 a7dd1a0df8f..86d01706fb6 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.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 7f6c45e131c..24d101ae4c9 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 2993e960b42..f0b18b4a20d 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 void dynamicTimelineUpdate_resolvesWindowIndices() { 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 void positionDiscontinuity_fromAdToAd_finishesPastAds_andNotifiesAdPlayba 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 ba615c4b91e..6094046b871 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 06f308d1e93..ef4bd00f20b 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 void setDownloadingStatesToQueued() throws DatabaseIOException { } } + @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 3bf03dd3e83..ec5ff81d97e 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 @@ default void onRequirementsStateChanged( 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 void removeDownload(String id) { 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 void handleMessage(Message message) { 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 @@ private void removeDownload(String id) { 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 @@ private Download putDownloadWithState(Download download, @Download.State int sta // 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 @@ private int getDownloadIndex(String id) { 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 fdd7163a2c8..3900dc8e938 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 static Intent buildRemoveDownloadIntent( .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 static void sendRemoveDownload( 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 int onStartCommand(Intent intent, int flags, int startId) { 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 ae634f8544b..dc7085c85e0 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 a3df647efe0..3a3067853e7 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 void secondSameRemoveRequestIgnored() throws Throwable { 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 @@ private DownloadRunner postRemoveRequest() { 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 18ca81c72f3..3b74240379f 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 @@ * 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 742f300df1c..34d8d0aa08c 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 @@ private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPerio && 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 2ac71db44f1..056038d97ab 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 @@ private void verifyApplicationThread() { 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 c2faaba823a..215a48fc508 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.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 @@ public interface ProvisioningManager { */ 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 @@ public interface ProvisioningManager { 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 @@ public interface ProvisioningManager { * @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 @@ public DefaultDrmSession( UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, + ReleaseCallback releaseCallback, @Nullable List schemeDatas, @DefaultDrmSessionManager.Mode int mode, @Nullable byte[] offlineLicenseKeySetId, @@ -145,6 +152,7 @@ public DefaultDrmSession( } this.uuid = uuid; this.provisioningManager = provisioningManager; + this.releaseCallback = releaseCallback; this.mediaDrm = mediaDrm; this.mode = mode; if (offlineLicenseKeySetId != null) { @@ -178,10 +186,9 @@ public void acquire() { } } - /** @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 @@ public boolean release() { sessionId = null; eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); } - return true; + releaseCallback.onSessionReleased(this); } - return false; } public boolean hasSessionId(byte[] sessionId) { @@ -330,8 +336,11 @@ private void doLicense(boolean allowRetry) { 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 @@ private void onKeyResponse(Object request, Object response) { } 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 @@ public void handleMessage(Message msg) { break; default: break; - } } - } @SuppressLint("HandlerLeak") @@ -541,6 +550,5 @@ private boolean maybeRetryRequest(Message originalMsg) { 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 3820836e49b..f27fefa0554 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.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 @@ private MissingSchemeDataException(UUID uuid) { * 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 final void removeListener(DefaultDrmSessionEventListener eventListener) { /** * 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 final String getPropertyString(String key) { /** * 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 final void setPropertyString(String key, String value) { /** * 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 final byte[] getPropertyByteArray(String key) { /** * 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 boolean canAcquireSession(DrmInitData drmInitData) { 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa new DefaultDrmSession<>( uuid, mediaDrm, - this, + /* provisioningManager= */ this, + /* releaseCallback= */ this::onSessionReleased, schemeDatas, mode, offlineLicenseKeySetId, @@ -444,17 +442,7 @@ public void releaseSession(DrmSession session) { // 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 void onProvisionError(Exception error) { // 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 @@ private static List getSchemeDatas( 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 void handleMessage(Message msg) { } } } - } private class MediaDrmEventListener implements OnEventListener { @@ -550,7 +548,5 @@ public void onEvent( @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 86d01706fb6..d3e8b49b561 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.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 24d101ae4c9..7f6c45e131c 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 6094046b871..080f5a0510e 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 ef233bb5669..2cfd14c79da 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 @@ private static Format deriveAudioFormat( 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 080f5a0510e..84c933c52c6 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 a2fe8244f3e..2e1bd19f478 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 @@ protected MediaFormat getMediaFormat( // 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 @@ private void updateCurrentPosition() { } } + /** + * 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 9a0339f5d4e..48578d88533 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 @@ final class EventTime { 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 @@ final class EventTime { 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 @@ final class EventTime { * @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 494af011a55..0bce1d4b82a 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 4b9bfa8fa2d..31ff0ad6b6e 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 84c933c52c6..6af3381a348 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 08ba94f2571..e79c776f88f 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 static MediaCodecInfo newPassthroughInstance(String name) { * * @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 static MediaCodecInfo newInstance(String name, String mimeType, * * @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 static MediaCodecInfo newInstance(String name, String mimeType, 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 f275dfe7d7c..f3936e5dc2f 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 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S */ 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 d256db8c301..e9d856015e3 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.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 @@ private void logDecoderInfos(String mimeType, boolean secure, boolean tunneling) 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 6af3381a348..5ff8644ce4b 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 d04cd3a9994..489719eda40 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 9cbcea5814e..97aac9995d6 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 void decode_quoteInTitle() { 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 a818ea390e2..74fcc3dd6c3 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 5ff8644ce4b..a067bb4cc91 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 2e1bd19f478..c3ec759c2d0 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.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 @@ && allowPassthrough(format.channelCount, mimeType) // 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 89c5dd66509..3b05bd1e413 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 static final class SchemeData implements 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 static final class SchemeData implements 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 SchemeData( * @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 SchemeData( licenseServerUrl = in.readString(); mimeType = Util.castNonNull(in.readString()); data = in.createByteArray(); - requiresSecureDecryption = in.readByte() != 0; } /** @@ -381,7 +358,7 @@ public boolean hasData() { * @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 void writeToParcel(Parcel dest, int flags) { 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 615aa0e7b12..848d9e146a1 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 @@ private static SchemeData getSchemeData(UUID uuid, List schemeDatas) for (int i = 0; i < schemeDatas.size(); i++) { SchemeData schemeData = schemeDatas.get(i); byte[] schemeDataData = Util.castNonNull(schemeData.data); - if (schemeData.requiresSecureDecryption == firstSchemeData.requiresSecureDecryption - && Util.areEqual(schemeData.mimeType, firstSchemeData.mimeType) + if (Util.areEqual(schemeData.mimeType, firstSchemeData.mimeType) && Util.areEqual(schemeData.licenseServerUrl, firstSchemeData.licenseServerUrl) && PsshAtomUtil.isPsshAtom(schemeDataData)) { concatenatedDataLength += schemeDataData.length; 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 611a906a9bc..fe9996bfc2b 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 @@ -298,29 +298,26 @@ protected int supportsFormat(MediaCodecSelector mediaCodecSelector, if (!MimeTypes.isVideo(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; } - boolean requiresSecureDecryption = false; DrmInitData drmInitData = format.drmInitData; - if (drmInitData != null) { - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; - } - } + // Assume encrypted content requires secure decoders. + boolean requiresSecureDecryption = drmInitData != null; List 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 0e3c6a8bda7..64ec1adb434 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 @@ protected Pair parseContentProtection(XmlPullParser xpp) 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 @@ protected Pair parseContentProtection(XmlPullParser xpp) 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 @@ protected Pair parseContentProtection(XmlPullParser xpp) } } 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 a067bb4cc91..bcdef404790 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 eb956f06db1..3f2fef454fa 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.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 @@ public List decode(byte[] data, int limit) { 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 @@ public List decode(byte[] data, int limit) { 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 @@ public List decode(byte[] data, int limit) { (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 bcdef404790..a7c998a2ef7 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 489719eda40..3d873926bbe 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 97aac9995d6..4602d172a66 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 void decode_titleOnly() { 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 7831033b99d..1076716684c 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 1dd1b7a62ea..3470c29e3cd 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 4fa24543d62..d53471d17ca 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 341ea2fb16d..16136b3d945 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 a9995af0e41..ca196b1d2f9 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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { * #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 CronetDataSource( public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -225,7 +239,7 @@ public CronetDataSource( public CronetDataSource( CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, + @Nullable Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -246,7 +260,7 @@ public CronetDataSource( /* 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 a749495184d..8eb8bba920f 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 6aad517004b..66036b7a841 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 6669504c07b..fd7be241dfb 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 @@ /** 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 05c804c7a82..8f2c891e3a7 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.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.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 @@ public void release() { 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 8f464006708..8ee9e9f9f69 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.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 @@ private DataSource.Factory buildDataSourceFactory() { 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 8c118105065..1f7a8d0ee74 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 755f7e03432..d2b7bd84d20 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.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.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 interface Callback { 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 static DownloadHelper forDash( 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 static DownloadHelper forHls( 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 static DownloadHelper forSmoothStreaming( 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 static DownloadHelper forSmoothStreaming( */ 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 static MediaSource createMediaSource( 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 @@ private TrackSelectorResult runTrackSelection(int periodIndex) { } } - 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 f3ed19db3d3..3951dc20a29 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.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 interface EventListener { /** 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 00000000000..c53abd12355 --- /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 f448b0b6c82..5ed12154b32 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.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 Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractor * 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 Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { * @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 Factory setCustomCacheKey(String customCacheKey) { * * @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 Factory setTag(Object tag) { * * @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 Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandl * 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 1998977961c..8828e34304f 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.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 @@ */ 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 cdc32553f33..709fd00ea71 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.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 Factory setManifestParser( 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 DashMediaSource createMediaSource( 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 DashMediaSource createMediaSource(Uri manifestUri) { 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 b6b874b2939..be4484aa78b 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.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 Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserF 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 Factory setUseSessionKeys(boolean useSessionKeys) { 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 HlsMediaSource createMediaSource(Uri playlistUri) { 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 09820f092d4..7b9f3e3c4fe 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.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 Factory setManifestParser(ParsingLoadable.Parser ma 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 SsMediaSource createMediaSource( 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 SsMediaSource createMediaSource(Uri manifestUri) { 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 1330bd066e0..afe53099ee5 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 @@ private boolean canDispatchMediaButtonEvent() { 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 void onPlay() { } 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 void onCommand(String command, Bundle extras, ResultReceiver cb) { @Override public void onPrepare() { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { - stopPlayerForPrepare(/* playWhenReady= */ false); + setPlayWhenReady(/* playWhenReady= */ false); playbackPreparer.onPrepare(); } } @@ -1189,7 +1188,7 @@ public void onPrepare() { @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 void onPrepareFromMediaId(String mediaId, Bundle extras) { @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 void onPrepareFromSearch(String query, Bundle extras) { @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 void onPrepareFromUri(Uri uri, Bundle extras) { @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 void onPlayFromMediaId(String mediaId, Bundle extras) { @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 void onPlayFromSearch(String query, Bundle extras) { @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 8fce472c68d..17b0996387b 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 @@ private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int 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 257f1c45b35..e9823697f7d 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 void testReadSingleBytes() throws IOException { // 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 c467bd36afe..20f6f436b00 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 @@ private FakeExtractorInput(byte[] data, boolean simulateUnknownLength, * @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 e860d07a141..bdeebec44c0 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 void resumeAd() { @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 a7c998a2ef7..ab42c4cc58e 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 0e668d24642..6e4cd58d096 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 72760db31bb..a90435227bb 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 8572c9c7ca1..e6679e1a5aa 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 @@ * * * - * * } 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 057f0262d01..2877b2a1cc9 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.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 void getNextChunk( 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 df386781ba9..86b3ab32e1e 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 afe53099ee5..d03e8fbdbf4 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 @@ boolean onCommand( 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 interface PlaybackPreparer extends CommandReceiver { * @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 @@ private boolean canDispatchMediaButtonEvent() { return player != null && mediaButtonEventHandler != null; } - private void setPlayWhenReady(boolean playWhenReady) { - if (player != null) { - controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady); - } - } - private void rewind(Player player) { if (player.isCurrentWindowSeekable() && rewindMs > 0) { seekTo(player, player.getCurrentPosition() - rewindMs); @@ -1045,19 +1061,19 @@ public void onPlay() { 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 void onCommand(String command, Bundle extras, ResultReceiver cb) { @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 86b3ab32e1e..a02bd629e5e 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 00000000000..b03dd0ea7c8 --- /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 aea8bda6638..35a5150a47a 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 82cffe17c2c..d6e09107a75 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 d53bca4ca22..6ec1cea289d 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 1d68b376ac3..509151aa214 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 9a4e33189ea..564c5d94ddd 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 ac6a3bf68f8..d17f889d172 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 7eab4e2d59b..b2e19827d92 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 a0123f17db2..851213e6533 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 2cf0313256d..72b555a25a1 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 e20c1fbb9fb..00892b77b8f 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 326ff48b16c..356a814026b 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 712169a7d05..df5643a1b2c 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 1a749dc82cf..b8f75629695 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 e30ea1c3ca4..edb8bcafde5 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 272c944e46c..544975ea035 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_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 void setAudioAttributes_withNullUsage_releasesAudioFocus() { } @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 void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocu } @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 void handleStop_withAudioFocus_abandonsAudioFocus() { } @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 void handleStop_withoutHandlingAudioFocus_isNoOp() { } @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 d41c99183d8..c0b5205455b 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 void doesNotSupportFloatOutputBeforeApi21() { .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 a02bd629e5e..bfe871b4cc5 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 552774fe47e..54a592ce6ff 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 interface VisibilityListener { 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 interface VisibilityListener { @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 void setVisibilityListener(VisibilityListener listener) { this.visibilityListener = listener; } + /** + * Sets the {@link ProgressUpdateListener}. + * + * @param listener The listener to be notified about when progress is updated. + */ + public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) { + this.progressUpdateListener = listener; + } + /** * Sets the {@link PlaybackPreparer}. * @@ -855,6 +877,9 @@ private void updateProgress() { 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 f4a7976ebd2..27e6a5b3b84 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 3b74240379f..90f8b125ac8 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.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 4f45e857626..4d51fb9b8e3 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.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 8cb0ac58c79..5e425cc12f7 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.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 bfe871b4cc5..b0544c1d1b4 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 00000000000..99f0dee2072 --- /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 c776898bc63..21467c3e252 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.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.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 PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { break; case SURFACE_TYPE_MONO360_VIEW: SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); - sphericalSurfaceView.setSurfaceListener(componentListener); sphericalSurfaceView.setSingleTapListener(componentListener); surfaceView = sphericalSurfaceView; break; @@ -1368,7 +1365,6 @@ private final class ComponentListener TextOutput, VideoListener, OnLayoutChangeListener, - SphericalSurfaceView.SurfaceListener, SingleTapListener { // TextOutput implementation @@ -1458,18 +1454,6 @@ public void onLayoutChange( 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 f7b208d085b..8f82ae17db5 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 @@ */ 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 interface SurfaceListener { 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 void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) } } - /** - * 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 @@ protected void onDetachedFromWindow() { 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 @@ private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { 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 f70125ba38a..bd768686250 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 d79dead0d76..c8975275f15 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 @@ * * @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 b0544c1d1b4..ed9635a340d 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 328b5d6a491..5c70203788f 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 DefaultTimeBar(Context context, AttributeSet attrs) { 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 54a592ce6ff..b9b24567225 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.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 @@ *

  • 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 @@ *
      *
    • 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 PlayerControlView( 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 21467c3e252..5bb83247801 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 @@ *
  • 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 @@ *
  • 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 PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { 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 ed2fb8e2b21..027e57ee928 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 27e6a5b3b84..706fba0e0b7 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 e57301f9460..17b55cd731b 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 272a8d1eb43..bf5b8e9261f 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 long open(DataSpec dataSpec) throws RtmpIOException { @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 void close() { } @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 270c1f6323a..7d549be7cb8 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 @@ 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 CronetEngineWrapper(CronetEngine cronetEngine) { * * @return A {@link CronetEngineSource} value. */ - public @CronetEngineSource int getCronetEngineSource() { + @CronetEngineSource + public int getCronetEngineSource() { return cronetEngineSource; } @@ -153,13 +155,14 @@ public CronetEngineWrapper(CronetEngine cronetEngine) { * * @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 bf5b8e9261f..366d1c120db 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 @@ 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 RtmpDataSource() { super(/* isNetwork= */ true); } - /** - * @param listener An optional listener. - * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - public RtmpDataSource(@Nullable TransferListener listener) { - this(); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws RtmpIOException { transferInitializing(dataSpec); 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 9224e14d4af..eeb0f1c957a 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 AssetDataSource(Context context) { 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 c723d3f1ca4..8df69ffb7ac 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 ContentDataSource(Context context) { 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 8b4107850c1..aa13baa03e5 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 @@ * 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 DefaultDataSource(Context context, DataSource baseDataSource) { 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 9639b4ede16..a5dfad72f03 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 DefaultDataSourceFactory( * @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 DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSour * @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 66036b7a841..65b65efe2cc 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 @@ * *

    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 DefaultHttpDataSource( 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 3cfdc4812b9..cead3663603 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 FileDataSource() { 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 1f0313594b7..7b70bcc5c44 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 RawResourceDataSource(Context context) { 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 e7aab31cc28..fcfeef3fb4c 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 UdpDataSource(int maxPacketSize, int socketTimeoutMillis) { 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 366d1c120db..587e310d648 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 @@ 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 473a91fedf8..f5cabf7c304 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 @@ 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 0a2c0c46f6b..15b36157fb8 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.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 c65ad0bc670..8f13cfaa116 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.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 f66c1f5d2ce..9bfe3831693 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 @@ 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 @@ 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 90f8b125ac8..ea45374f866 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 @@ 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 4d51fb9b8e3..e0673dd4fae 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 e9c9f7faf59..bec2cdbb5f5 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.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 @@ 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 75966bff663..16f5b1fb29c 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.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 5c5afe39a82..95193785c05 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 @@ 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 @@ 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 @@ private static boolean sniffInternal(ExtractorInput input, boolean fragmented) */ 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 bbf7e2fc6bd..ff32ae34629 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.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 @@ 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 ff5f1155731..90ae3f0f47d 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.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 @@ */ 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 aa77aba30ea..3d76276240a 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.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 889a49755ad..0a0755327c5 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.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 133c0f368b6..4db02e0d83a 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.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 9526a657668..a636d2f6802 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.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 a2f8568cbba..d198e816d51 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 c7b7a40ead3..7a6a7e346fe 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.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 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) 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 fff0828b3a5..44171264272 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 interface FramePredicate { 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 75fe8fed256..cdc545e459e 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.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 9211dc51ce3..89017a40c05 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 5e425cc12f7..b977f61a8a4 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 527aa5db4f9..eadc617ea73 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 @@ */ 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 a91bbbd981a..2df9448d081 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 0de29eea32a..2c5b64f8bd6 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 eeb0f1c957a..3c92b039cc8 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 AssetDataSourceException(IOException cause) { 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 AssetDataSource(Context context) { 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 int read(byte[] buffer, int offset, int readLength) throws AssetDataSourc 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 int read(byte[] buffer, int offset, int readLength) throws AssetDataSourc } @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 4017c1f0288..a9f9da0a952 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 void open(DataSpec dataSpec) throws IOException { @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 c4508966764..ed5ba9064b6 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 long open(DataSpec dataSpec) throws IOException { } @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 int read(byte[] buffer, int offset, int readLength) throws IOException { } @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 8df69ffb7ac..baaa677127e 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 ContentDataSourceException(IOException cause) { 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 ContentDataSource(Context context) { @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 int read(byte[] buffer, int offset, int readLength) throws ContentDataSou 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 int read(byte[] buffer, int offset, int readLength) throws ContentDataSou } @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 de4a75d607c..03804fa577d 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 long open(DataSpec dataSpec) throws IOException { 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 int read(byte[] buffer, int offset, int readLength) { 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 026bc0b9c77..4124a2531ff 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 long open(DataSpec dataSpec) throws IOException { } @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 cead3663603..e329dc722e6 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 FileDataSourceException(IOException cause) { } - 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 FileDataSource() { @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 int read(byte[] buffer, int offset, int readLength) throws FileDataSource } 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 int read(byte[] buffer, int offset, int readLength) throws FileDataSource } @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 7b70bcc5c44..ff032a4ed01 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 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 static Uri buildRawResourceUri(int rawResourceId) { 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 RawResourceDataSource(Context context) { @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 int read(byte[] buffer, int offset, int readLength) throws RawResourceDat 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 int read(byte[] buffer, int offset, int readLength) throws RawResourceDat } @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 fcfeef3fb4c..4d9b375334d 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 UdpDataSourceException(IOException cause) { 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 int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceE } @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 ccf9a5b3f50..522fdc9a3f2 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 AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) { * @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 void open(DataSpec dataSpec) throws IOException { 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 7a7af6b8a47..644338c8eba 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 int read(byte[] data, int offset, int readLength) throws IOException { 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 ff8841fa9c8..3418f46ed0e 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 @@ private CryptoUtil() {} /** * 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 30cf4525724..882d9def3ca 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.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 @@ private static boolean isInternetConnectivityValidated(ConnectivityManager conne } @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 f0d0f37cdf1..b9cbf681b1b 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.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 @@ void onRequirementsStateChanged( 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 int start() { /** 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 @@ private void unregisterNetworkCallback() { 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 b863d80c9aa..a7ab93a6dd4 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.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 static CaptionStyleCompat createFromCaptionStyle( * @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 4b54b3ea9aa..29facdb2106 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 @@ 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 Cue(CharSequence text) { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -376,7 +373,7 @@ public Cue( */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -403,9 +400,9 @@ public 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 75b7a016738..b34628b9227 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 @@ */ 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 81c362bda5d..193b92678b7 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 @@ public CssParser() { } /** - * 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 @@ public WebvttCssStyle parseBlock(ParsableByteArray input) { } /** - * 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 @@ private static String parseSelector(ParsableByteArray input, StringBuilder strin target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); - if (!")".equals(token) || token == null) { + if (!")".equals(token)) { return null; } return target; @@ -196,6 +199,7 @@ private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyl } // Visible for testing. + @Nullable /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() == 0) { @@ -237,6 +241,7 @@ private static char peekCharAtPosition(ParsableByteArray input, int position) { return (char) input.data[position]; } + @Nullable private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) { StringBuilder expressionBuilder = new StringBuilder(); String token; @@ -325,7 +330,7 @@ private void applySelectorToStyle(WebvttCssStyle style, String selector) { 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 97bcb687080..4dfb8b50d56 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 static T[] nullSafeArrayCopy(T[] input, int length) { 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 6c6ddf4ce46..7ee973723c4 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 d03e8fbdbf4..9ec3886df55 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.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 interface CustomActionProvider { * @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 void invalidateMediaSessionMetadata() { */ 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 void invalidateMediaSessionQueue() { * * @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 void registerCustomCommandReceiver(CommandReceiver commandReceiver) { * * @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 @@ private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) } } + @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 MediaMetadataCompat getMetadata(Player player) { } } } - 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 @@ private class ComponentListener extends MediaSessionCompat.Callback @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 void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { @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 617b8781f47..5c969dd44de 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 @@ 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 RepeatModeActionProvider( @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 f532ae0e6a5..5b285411d0d 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 | 146 ++++++++++-------- library/core/build.gradle | 1 + 3 files changed, 86 insertions(+), 62 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 76972a35301..0808ad6c447 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 ca196b1d2f9..0ef20e79bd4 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.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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { // 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 CronetDataSource( * @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 CronetDataSource( int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties) { + @Nullable RequestProperties defaultRequestProperties) { this( cronetEngine, executor, @@ -232,7 +236,8 @@ public CronetDataSource( * @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 CronetDataSource( int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties, + @Nullable RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { this( cronetEngine, @@ -265,7 +270,7 @@ public CronetDataSource( 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 Map> getResponseHeaders() { } @Override + @Nullable public Uri getUri() { return responseInfo == null ? null : Uri.parse(responseInfo.getUrl()); } @@ -317,22 +323,23 @@ public long open(DataSpec dataSpec) throws HttpDataSourceException { 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 long open(DataSpec dataSpec) throws HttpDataSourceException { } // 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 long open(DataSpec dataSpec) throws HttpDataSourceException { responseCode, responseInfo.getHttpStatusText(), responseInfo.getAllHeaders(), - currentDataSpec); + dataSpec); if (responseCode == 416) { exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); } @@ -358,8 +366,8 @@ public long open(DataSpec dataSpec) throws HttpDataSourceException { 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 long open(DataSpec dataSpec) throws HttpDataSourceException { } 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 int read(byte[] buffer, int offset, int readLength) throws 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 int read(byte[] buffer, int offset, int readLength) throws 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 void onStatus(int status) { 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 synchronized void onRedirectReceived( 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 synchronized void onRedirectReceived( resetConnectTimeout(); } - Map> headers = info.getAllHeaders(); - if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) { + if (!handleSetCookieRequests) { + request.followRedirect(); + 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 { - 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(); + 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 5b285411d0d..f532ae0e6a5 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 ed9635a340d..06c1ed7f80d 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 d2b7bd84d20..e7cf87ed6ec 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 @@ private boolean handleDownloadHelperCallbackMessage(Message msg) { 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 690069ffa8e..a4996278bd1 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 f326e7cd46c..ff29f3a7d1c 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 089de359109..d481de33cea 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 3ed569862fd..a68e4e70e14 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 06b76781b48..730868987aa 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.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -742,11 +741,11 @@ private void maybeInitCodecWithFallback( 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 215a48fc508..94f5affb390 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 @@ public interface ProvisioningManager { public interface ReleaseCallback { /** - * Called when the session is released. + * Called immediately after releasing session resources. * * @param session The session. */ @@ -85,7 +85,7 @@ public interface ReleaseCallback { 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 @@ public interface ReleaseCallback { 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 @@ public DefaultDrmSession( 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 @@ public void onMediaDrmEvent(int what) { 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 @@ private void doLicense(boolean allowRetry) { } 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 @@ private void postKeyRequest(byte[] scope, int type, boolean allowRetry) { 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 83ca7521145..d6b0b5ba157 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 @@ 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 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 06c1ed7f80d..deda085f409 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 fcee88ec490..a837bd77e57 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.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 @@ private static MediaSource buildMediaSource(MediaItem item) { 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 96253042523..9599da15cb7 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 @@ /** 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 @@ public String toString() { } } + /** 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 17eeed2da76..5ed434eed6f 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.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 void onResume() { 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 @@ private View buildSampleListView() { .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 06f0bec971a..5aed1373e5e 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 @@ */ 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 | 60 +++++++-- .../ext/okhttp/OkHttpDataSourceFactory.java | 1 - .../upstream/DefaultHttpDataSource.java | 75 ++++++++++- .../DefaultHttpDataSourceFactory.java | 1 - 7 files changed, 271 insertions(+), 104 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 0ef20e79bd4..dd10e5bb661 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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { 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 InterruptedIOException(InterruptedException e) { * 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 CronetDataSource(CronetEngine cronetEngine, Executor executor) { * @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 CronetDataSource( contentTypePredicate, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, - false, - null, - false); + /* resetTimeoutOnRedirects= */ false, + /* defaultRequestProperties= */ null); } /** @@ -200,9 +274,12 @@ public CronetDataSource( * @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 CronetDataSource( connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, - Clock.DEFAULT, defaultRequestProperties, - false); + /* handleSetCookieRequests= */ false); } /** @@ -236,11 +312,14 @@ public CronetDataSource( * @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 CronetDataSource( 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 CronetDataSource( 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 CronetDataSource( 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 long open(DataSpec dataSpec) throws HttpDataSourceException { } // 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 93edb4e893b..4086011b4f3 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.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 CronetDataSourceFactory( * * @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 CronetDataSourceFactory( * * @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 CronetDataSourceFactory( public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -151,7 +134,6 @@ public CronetDataSourceFactory( this( cronetEngineWrapper, executor, - contentTypePredicate, /* transferListener= */ null, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, @@ -172,9 +154,6 @@ public CronetDataSourceFactory( * * @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 CronetDataSourceFactory( public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, @@ -192,7 +170,6 @@ public CronetDataSourceFactory( this( cronetEngineWrapper, executor, - contentTypePredicate, /* transferListener= */ null, connectTimeoutMs, readTimeoutMs, @@ -212,9 +189,6 @@ public CronetDataSourceFactory( * * @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 CronetDataSourceFactory( 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 CronetDataSourceFactory( * * @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 CronetDataSourceFactory( * * @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 CronetDataSourceFactory( 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 CronetDataSourceFactory( * * @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 CronetDataSourceFactory( public CronetDataSourceFactory( CronetEngineWrapper cronetEngineWrapper, Executor executor, - Predicate contentTypePredicate, @Nullable TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, @@ -320,7 +301,6 @@ public CronetDataSourceFactory( 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 @@ protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperti 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 7c4c03dd87b..df36076899a 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.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 void testRequestOpenValidatesStatusCode() { @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 void testRequestOpenValidatesContentTypePredicate() { 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 void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_fo new CronetDataSource( mockCronetEngine, mockExecutor, - mockContentTypePredicate, TEST_CONNECT_TIMEOUT_MS, TEST_READ_TIMEOUT_MS, true, // resetTimeoutOnRedirects @@ -765,13 +767,12 @@ public void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_fo 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 void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() 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 8eb8bba920f..eaa305875bf 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; - - private @Nullable DataSpec dataSpec; - private @Nullable Response response; - private @Nullable InputStream responseByteStream; + @Nullable private final String userAgent; + @Nullable private final CacheControl cacheControl; + @Nullable private final RequestProperties defaultRequestProperties; + + @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 OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { * @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 OkHttpDataSource( * 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 OkHttpDataSource( 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 d0ef35cb074..f18e37c5c4f 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 @@ protected OkHttpDataSource createDataSourceInternal( 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 65b65efe2cc..5955a5d9d94 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 DefaultHttpDataSource(String userAgent) { * @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 DefaultHttpDataSource(String userAgent, @Nullable Predicate conte * 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 DefaultHttpDataSource( * 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 DefaultHttpDataSource( 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 371343857f4..e0b1efad542 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 @@ protected DefaultHttpDataSource createDataSourceInternal( 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 4a3505749a4..db197643185 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 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 @@ * @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 @@ public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedU * @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 @@ public final Period getPeriod(int periodIndex, Period period, boolean setIds) { 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 c0780531104..d57dccd8fec 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.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 TrackGroupArray 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) { 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 @@ && shouldKeepInitialDiscontinuity(startUs, selections) 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 boolean continueLoading(long positionUs) { @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 @@ private SeekParameters clipSeekParameters(long positionUs, SeekParameters seekPa } } - 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 int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, } 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 int skipData(long positionUs) { } 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 ce6254e975a..c3e700fff58 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 @@ private static String getReasonDescription(@Reason int reason) { 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 void releasePeriod(MediaPeriod mediaPeriod) { 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 abf02541c84..95a218bfe77 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.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 interface PrepareErrorListener { 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 void maybeThrowPrepareError() throws IOException { @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 boolean continueLoading(long positionUs) { @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 3951dc20a29..841f18bab4c 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 ExtractorMediaSource( 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 ExtractorMediaSource( 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 ExtractorMediaSource( 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 a4fc8c6b00b..cafc052f34b 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.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 @@ 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 @@ public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceab compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); + enabledPeriods = new MediaPeriod[0]; } @Override @@ -69,12 +71,16 @@ public void maybeThrowPrepareError() throws IOException { @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 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF } 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 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF 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 @@ public long seekToUs(long positionUs) { @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 @@ public void onPrepared(MediaPeriod preparedPeriod) { } } 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 6b1a362b59b..7188cada0f0 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 IllegalMergeException(@Reason int reason) { 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 @@ protected void onChildSourceInfoRefreshed( } @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 e0c2a00df36..6063168e995 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.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 @@ /* 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 @@ public TrackGroupArray 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) { 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 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame 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 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 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 @@ private void maybeNotifyDownstreamFormat() { 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 be9dea91f1c..0a1628b3f96 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 @@ 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 static final class AdGroup { /** 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 AdGroup() { /* 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 boolean hasUnplayedAds() { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -130,7 +134,7 @@ public AdGroup withAdCount(int count) { 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 AdGroup withAdUri(Uri uri, int index) { 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 AdGroup withAdState(@AdState int state, int index) { 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 AdPlaybackState withAdCount(int adGroupIndex, int adCount) { 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 AdPlaybackState withAdCount(int adGroupIndex, int adCount) { /** 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 AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri /** 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 AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) { /** 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 AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { /** 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 AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { */ @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 AdPlaybackState withSkippedAdGroup(int adGroupIndex) { /** 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 AdPlaybackState withContentDurationUs(long contentDurationUs) { } @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 8828e34304f..78b0f6de11e 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.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 @@ private AdLoadException(@Type int type, Exception cause) { */ public RuntimeException getRuntimeExceptionForUnexpected() { Assertions.checkState(type == TYPE_UNEXPECTED); - return (RuntimeException) getCause(); + return (RuntimeException) Assertions.checkNotNull(getCause()); } } @@ -131,12 +132,12 @@ public RuntimeException getRuntimeExceptionForUnexpected() { 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 void prepareSourceInternal(@Nullable TransferListener mediaTransferListen @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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 void releasePeriod(MediaPeriod mediaPeriod) { @Override public void releaseSourceInternal() { super.releaseSourceInternal(); - componentListener.release(); + Assertions.checkNotNull(componentListener).release(); componentListener = null; deferredMediaPeriodByAdMediaSource.clear(); contentTimeline = null; @@ -305,7 +308,7 @@ private void onAdPlaybackState(AdPlaybackState adPlaybackState) { 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 @@ private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, } private void maybeUpdateSourceInfo() { + Timeline contentTimeline = this.contentTimeline; if (adPlaybackState != null && contentTimeline != null) { adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); Timeline timeline = @@ -340,7 +344,8 @@ private void maybeUpdateSourceInfo() { } } - 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 65039b93643..434b6c20114 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 @@ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStre 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 8ee9e9f9f69..f7db8c7ca30 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 @@ private DataSource.Factory buildDataSourceFactory() { } /** 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 7c5864420ac..12c26ca2ecc 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 @@ 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 6008d994489..3beb4d0103f 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 @@ private int seekToTimeUs( } } - 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 bb72e114fe0..79350e6ae3c 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 bdeebec44c0..1cdbac56b58 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 ImaAdsLoader buildForAdsResponse(String adsResponse) { */ 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 1e1935c63a8..ab880703ee0 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 AdEventType getType() { } @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 5705b73ab20..1fece6bc8e9 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 eaa305875bf..ec05c52f447 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 void setContentTypePredicate(@Nullable Predicate contentTypePredi } @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 f18e37c5c4f..f3d74f92330 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 @@ 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 36abf825d67..505724e846e 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 @@ */ 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 89e7d857c83..bcec6426d67 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 @@ public interface PlaybackParameterListener { 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 bea7af189af..de6be33686f 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 @@ 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 @@ public int getPlaybackState() { } @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 34d8d0aa08c..ff94567a488 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 @@ private static final class PendingMessageInfo implements Comparable 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 249548340e3..58000d6f580 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 @@ 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 cf4643c5da4..d3e4a0e6269 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 @@ /** 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 7904942c1b9..49309181a06 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 interface Sender { 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 PlayerMessage setPayload(@Nullable Object payload) { } /** 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 056038d97ab..da66f3dd105 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 int getPlaybackState() { } @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 094024bc366..deecfb15a8e 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 @@ private static final class MediaPeriodQueueTracker { 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 MediaPeriodQueueTracker() { * 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 MediaPeriodQueueTracker() { * 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 MediaPeriodQueueTracker() { * 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 MediaPeriodQueueTracker() { * 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 boolean isSeeking() { * 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 48578d88533..be62ad99d26 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 @@ final class EventTime { * 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 3cc05e87dfd..2d65b64f366 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 interface PlayerControl { 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 d43972d7b0b..0564591f1f2 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 @@ */ 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 e87e49d2da6..4ee70bd8132 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 @@ void onSystemTimeUsMismatch( 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 @@ void onSystemTimeUsMismatch( 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 94f5affb390..e300c655929 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 @@ public interface ReleaseCallback { 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 @@ public interface ReleaseCallback { 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 @@ public final int getState() { } @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 f27fefa0554..7481c60c64e 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 @@ private MissingSchemeDataException(UUID uuid) { 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 @@ private MissingSchemeDataException(UUID uuid) { 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 3b05bd1e413..7cc2231e0f0 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 @@ private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas, * @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 static final class SchemeData implements 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 82fd9a55495..bcc0739042a 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 int getState() { } @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 caf12948ad5..f6b64245fc1 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 116a1230944..c0c2080e172 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 @@ * 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 9bfe3831693..572efed1af5 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 @@ public void add(ContainerAtom atom) { * @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 @@ public void add(ContainerAtom atom) { * @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 e0673dd4fae..392d4d91790 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 a636d2f6802..d1e3217e308 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 730868987aa..cd043655ec6 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 static class DecoderInitializationException extends Exception { * 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 41cb4ee04a0..a639cf9a1bd 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 List getDecoderInfos( } @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 e34b4074fb2..a72c70442e5 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 interface Output extends MetadataOutput {} 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 c233ad61b29..d4bedc63cc4 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 44171264272..85a59c3aebc 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 interface FramePredicate { 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 Id3Decoder(@Nullable FramePredicate framePredicate) { @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 Id3Decoder(@Nullable FramePredicate framePredicate) { * @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 8a36276b919..0e129ca7bb6 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 @@ */ 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 8be9ed18810..298558b662c 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 @@ */ 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 9323f7505cd..06db088f06e 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 841f18bab4c..d9003e443ec 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 static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - private @Nullable ExtractorsFactory extractorsFactory; - private @Nullable String customCacheKey; - private @Nullable Object tag; + @Nullable private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; 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 233e19b29c8..ab8d86cc559 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 @@ final class MediaLoadData { * 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 @@ final class MediaLoadData { * 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 @@ final class EventDispatcher { /** 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 d9f0008a7fc..e8f630f202d 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 @@ private static final class ExtractorHolder { 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 acdfbcc8c04..14648775f8d 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 6063168e995..62d873868e8 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 @@ 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 6f85a2b0f86..55d967cd691 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 static final class Factory { 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 SingleSampleMediaSource createMediaSource( 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 2e7581eba56..a794f67fe2d 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 18eada4708d..d7a19fa9d44 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 interface ReleaseCallback { 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 55bee5bd6a4..bdf127be59d 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 interface Output extends TextOutput {} 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 bbf57c56024..08f4d3a9284 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 int getSelectionReason() { } @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 3bdaeeeafba..fefad00cbd1 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 Factory(int reason, @Nullable Object data) { } 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 int getSelectionReason() { } @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 dfb19e3bcaf..5587af9cbfb 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 TrackGroupArray getUnmappedTrackGroups() { } - 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 80532129693..f35e7ec755a 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 int getSelectionReason() { } @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 bc905ace4b4..fc20e863bac 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 TrackSelectionArray(@NullableType TrackSelection... trackSelections) { * @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 f2fbd891188..fb74bd9d54c 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 interface InvalidationListener { } - 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 21f2d5993a2..80687db31f8 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 a98f773c9d6..99a3d271bdf 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 aa13baa03e5..bfc9a378441 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 int read(byte[] buffer, int offset, int readLength) throws IOException { } @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 a5dfad72f03..6b1131a3bd4 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 @@ 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 5955a5d9d94..0d16a3f20e4 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 void setContentTypePredicate(@Nullable Predicate contentTypePredi } @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 e0b1efad542..f5d7dbd24c0 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 @@ 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 3a47df7654f..0b4de1b43ef 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 @@ */ 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 878c40dc9e3..b5a13f3b80e 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 @@ private final class LoadTask extends Handler implements Runn 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 62e68cd920c..767b6d78a37 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 int read(byte[] buffer, int offset, int max) throws IOException { } @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 b7a01505f82..6cdc381ba2d 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 int read(byte[] buffer, int offset, int readLength) throws IOException { } @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 ecf25f2eb69..f56f19a6ca3 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 int read(byte[] buffer, int offset, int max) throws IOException { } @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 58b2d176cf2..058489f8f01 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 interface EventListener { 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 interface EventListener { 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 int read(byte[] buffer, int offset, int readLength) throws IOException { } @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 1e8cf1517d7..609e933c9d3 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 644338c8eba..0910c63c19e 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 int read(byte[] data, int offset, int readLength) throws IOException { } @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 33b50934f12..e72e72c3c42 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 @@ private GlException(String msg) { 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 7a2ea5daf2a..cde9a351d99 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 0c5116624ee..67686ad64fc 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 String readNullTerminatedString(int length) { * @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 String readNullTerminatedString(int length) { * @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 3ac76eb54cc..da5d9bafeb5 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 synchronized int size() { * @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 1b3943caf71..ed2ca9c0340 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 f302279f066..920d569fd3a 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 @@ private static class DummySurfaceThread extends HandlerThread implements Callbac 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 727883f678e..bb11ef0005b 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 @@ */ 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 fe9996bfc2b..7193c4c22bf 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 eb7110834bc..03822be17c7 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 2e9b5390966..22aa63b83ad 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 @@ protected void onBufferRead() { 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 2426073d8a6..1eb49188bfd 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 int read(byte[] buffer, int offset, int readLength) throws IOException { } @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 aa080bbdec3..be0aa4f1545 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 @@ /* 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 @@ 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 709fd00ea71..779a97fd09c 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 int[] getSupportedTypes() { 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 @@ private static final class DashTimeline extends Timeline { 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 057f0262d01..2de81a2535e 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 @@ protected static final class RepresentationHolder { /* 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 c7bb4adec5e..9ac1257ee2f 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 String resolveUriString(String baseUri) { * @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 4fe76cdf819..022d62cbfc2 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 @@ 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 2cfd14c79da..d834c097cf5 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 be4484aa78b..fbb6285d1d5 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 int[] getSupportedTypes() { 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 0064338ca8a..a4fd28009f9 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 void removeListener(PlaylistEventListener listener) { } @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 135ee4a58e4..38781782eba 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 @@ 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 @@ 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 7b9f3e3c4fe..3c18bfe6445 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 int[] getSupportedTypes() { 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 5c70203788f..69a2cf96be7 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 5bb83247801..3575969a78f 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 void setUseArtwork(boolean useArtwork) { } /** 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 f24bcce3ceb..8a211d0879e 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 @@ public static boolean isSupported(Projection projection) { }; 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 2889351f198..b70fd277a97 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 8f82ae17db5..67bc9925581 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 142f2fc668b..5f3a5275c19 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 @@ // 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 facbe8bbde9..93e52bc23a5 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 @@ 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 @@ protected void doActionImpl( */ 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 7f688cacf71..735156e64c4 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 @@ protected void doActionImpl( */ 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 0d55dd85305..cc4b3a60d73 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 ExoPlayerTestRunner build(Context context) { 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 1e3b3bf82b5..fea863c48ef 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 77ae19f0834..286ef15b159 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 static final class FakeData { */ 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 b89acae6c8b..0d50f22bc09 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 a60c1c9c6d4..02d0e372e8d 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 @@ 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 f7db8c7ca30..82fb8bb9f52 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 4f22362de66..9ed1bbd0063 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 @@ private void setupBitmapLayout() { 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 d5da9a011d6..5871371d765 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 @@ protected void releaseDecoder() { } 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 @@ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackExceptio } 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 5da8d0f9f53..bd73eaf4fbc 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 553dfb11877..15532279888 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 @@ private void releaseDecoder() { } 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 @@ private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException } 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 e300c655929..e49602957fe 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.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 @@ public interface ReleaseCallback { /* 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 @@ public DefaultDrmSession( 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 @@ public byte[] getOfflineLicenseKeySetId() { return offlineLicenseKeySetId; } + @Override + public void acquireReference() { + if (++referenceCount == 1) { + Assertions.checkState(state == STATE_OPENING); + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); + if (openInternal(true)) { + doLicense(true); + } + } + } + + @Override + public void releaseReference() { + if (--referenceCount == 0) { + // Assigning null to various non-null variables for clean-up. + state = STATE_RELEASED; + Util.castNonNull(postResponseHandler).removeCallbacksAndMessages(null); + Util.castNonNull(postRequestHandler).removeCallbacksAndMessages(null); + postRequestHandler = null; + Util.castNonNull(requestHandlerThread).quit(); + requestHandlerThread = null; + mediaCrypto = null; + lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); + } + releaseCallback.onSessionReleased(this); + } + } + // Internal methods. /** @@ -288,9 +289,10 @@ private boolean openInternal(boolean allowProvisioning) { 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 @@ private void onProvisionResponse(Object request, Object response) { @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 @@ private void doLicense(boolean allowRetry) { 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 7481c60c64e..84e984445a2 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa 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 392b0734b17..761fb74287b 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 @@ */ 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 DrmSessionException(Throwable cause) { */ @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 d8093507a43..168783cf1c0 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 bcc0739042a..d40cf609069 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 byte[] getOfflineLicenseKeySetId() { return null; } + @Override + public void acquireReference() { + // Do nothing. + } + + @Override + public void releaseReference() { + // Do nothing. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 55a7a901ac7..05dab7e42d7 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 synchronized Pair getLicenseDurationRemainingSec(byte[] offli 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 @@ private byte[] blockingKeyRequest( 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 cd043655ec6..05f83109e83 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 @@ private void resetOutputBuffer() { } 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 @@ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybac } 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 2c5b64f8bd6..be75eae3596 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 59bf5f8f870..ed28f07acb1 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 eab68625556..18f1dd5c698 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 e8f630f202d..dbf5f8aa5d2 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 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame 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 25cc73d4aee..b2c09bd70f0 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 @@ public synchronized void rewind() { * @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 @@ public synchronized int read( FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean allowOnlyClearBuffers, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { @@ -264,6 +267,10 @@ public synchronized int read( 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 e8f49534369..976a5d4e48f 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 boolean setReadPosition(int sampleIndex) { * @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 int read( 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 d7a19fa9d44..d9b28d9c92e 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 int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, } maybeNotifyPrimaryTrackFormatChanged(); return primarySampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + decodeOnlyUntilPositionUs); } @Override @@ -801,7 +806,12 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, } 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 450f0ecd3a0..bfc6bb52c98 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.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 void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() { 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 @@ private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets, 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 @@ private void assertNoSamplesToRead(Format endFormat) { /** * 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 @@ private void assertReadNothing(boolean formatRequired) { } /** - * 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 @@ private void assertReadEndOfStream(boolean formatRequired) { * 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 @@ private void assertReadFormat(boolean formatRequired, Format format) { 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 @@ private void assertReadSample( 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 34e1ecc2b67..af4bf3ad704 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 @@ private void parseAndDiscardSamples() { @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 434b6c20114..96704053cbe 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 @@ && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) { 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 deda085f409..edddbbe7ec6 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 2a977f5bba6..490d9613962 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 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 DefaultRenderersFactory setPlayClearSamplesWithoutKeys( 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 Renderer[] createRenderers( mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, videoRendererEventListener, allowedVideoJoiningTimeMs, @@ -258,6 +274,7 @@ public Renderer[] createRenderers( mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, buildAudioProcessors(), eventHandler, audioRendererEventListener, @@ -282,6 +299,9 @@ public Renderer[] createRenderers( * @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 @@ protected void buildVideoRenderers( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, @@ -305,6 +326,7 @@ protected void buildVideoRenderers( allowedVideoJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, + enableDecoderFallback, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -356,6 +378,9 @@ protected void buildVideoRenderers( * @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 @@ protected void buildAudioRenderers( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @@ -378,10 +404,10 @@ protected void buildAudioRenderers( 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 c3ec759c2d0..d83e32c61c4 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 MediaCodecAudioRenderer( @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 2b479c549a2..6bd4c8dd14d 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 @@ protected void buildVideoRenderers( 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 22330e0a053..30d7b8e92c1 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 void release() { * 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 boolean initForYuvFrame(int width, int height, int yStride, int 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); diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 82c023afbc2..9fc8b09a18c 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 dbe5c931722..00000000000 --- 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 df36076899a..a01c5e84b64 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 @@ 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 d6b0b5ba157..886be4c4760 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 @@ 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 b077a92d996..91e7393fe7c 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 b00ee73f0f2..73780f56f3b 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 dc9c0e06449..f389944670e 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 afe6a9879b8..8ded5038b02 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 @@ private 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 @@ private 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 @@ private 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 cf914567d6f..425b0994b61 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 @@ private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) 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 @@ private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffe 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 d83e32c61c4..fe8e898b06b 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 @@ protected List getDecoderInfos( * @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 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) @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 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) } } + /** + * 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 e603f76dbc3..61457c308da 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 static int getTrackType(@Nullable String mimeType) { 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 c53c4337b59..38c43bd5517 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 synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long p 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 synchronized ContentMetadata getContentMetadata(String key) { 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 @@ private void loadDirectory( } } + /** + * 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 edddbbe7ec6..3b1ccc3d432 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 b39f467968d..6e0c495466c 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 @@ private TtmlRegion parseRegionAttributes( /* 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 ecf5c8b0a0e..3b4d061aaaa 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 @@ public List getCues( 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 2b1e9cf99af..3cbc25d4b24 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 @@ 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 @@ public TtmlRegion(String id) { /* 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 @@ public TtmlRegion( @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float width, + float height, int textSizeType, float textSize) { this.id = id; @@ -58,6 +61,7 @@ public TtmlRegion( 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 000d0634ce6..85af6482c0e 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 void testBitmapPercentageRegion() throws IOException, SubtitleDecoderExce 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 void testBitmapPercentageRegion() throws IOException, SubtitleDecoderExce 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 void testBitmapPercentageRegion() throws IOException, SubtitleDecoderExce 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 void testBitmapPixelRegion() throws IOException, SubtitleDecoderException 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 void testBitmapPixelRegion() throws IOException, SubtitleDecoderException 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 38c43bd5517..1d4481b5cce 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 synchronized SimpleCacheSpan startReadWrite(String key, long position) if (span != null) { return span; } else { - // Write case, lock not available. We'll be woken up when a locked span is released (if the - // released lock is for the requested key then we'll be able to make progress) or when a - // span is added to the cache (if the span is for the requested key and covers the requested - // position, then we'll become a read and be able to make progress). + // Lock not available. We'll be woken up when a span is added, or when a locked span is + // released. We'll be able to make progress when either: + // 1. A span is added for the requested key that covers the requested position, in which + // case a read can be started. + // 2. The lock for the requested key is released, in which case a write can be started. wait(); } } @@ -415,12 +416,12 @@ public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long p 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 3b1ccc3d432..b73d95ba034 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 058489f8f01..69bb99451ed 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 interface EventListener { 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 int read(byte[] buffer, int offset, int readLength) throws IOException { } 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 @@ private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaul 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 219d736835e..9c80becdebc 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 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 static void cache( 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 static void cache( * 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 @@ private static long readAndDiscard( PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -263,22 +271,23 @@ private static long readAndDiscard( // 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 static void remove(Cache cache, String key) { } } + /*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 9a179043793..a48f88b5c08 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 static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... * @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 static void assertDataCached(Cache cache, Uri uri, byte[] expected) throw 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 9c80becdebc..5b066b79301 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 static Pair getCached( 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 static void cache( 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 static void cache( } } + 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 | 98 ++++++++++++------- 4 files changed, 88 insertions(+), 53 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 b3a6b8271bf..94dae35ed55 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.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 void testDownloadRepresentation() throws Exception { 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 void testDownloadRepresentationInSmallParts() throws Exception { 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 void testDownloadRepresentations() throws Exception { 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 void testDownloadAllRepresentations() throws Exception { 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 void testDownloadRepresentationFailure() throws Exception { // 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 56fedbefd0d..bc75df6acf8 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.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 void testSaveAndLoadActionFile() throws Throwable { 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 void testHandleMultipleDownloadRequest() throws Throwable { handleDownloadRequest(fakeStreamKey1); handleDownloadRequest(fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -175,7 +176,7 @@ public void testHandleInterferingDownloadRequest() throws Throwable { 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 7d77a78316e..d06d047f669 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.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 void testDownloadRepresentation() throws Exception { 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 void testDownloadMediaPlaylist() throws Exception { 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 a48f88b5c08..4ea4c0844eb 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 @@ /** 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); + } } - 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, String... uriStrings) - throws IOException { - Uri[] uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); + 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, 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 dd10e5bb661..a1ee80767d3 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 long open(DataSpec dataSpec) throws HttpDataSourceException { 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 @@ private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOExcep } 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 @@ private void resetConnectTimeout() { 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 0d16a3f20e4..3ee1ef75646 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.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 long open(DataSpec dataSpec) throws HttpDataSourceException { 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 long open(DataSpec dataSpec) throws HttpDataSourceException { } } 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 @@ private HttpURLConnection makeConnection( 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 @@ private void closeConnectionQuietly() { } } + 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 b73d95ba034..f98e3dfe845 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 3575969a78f..7e01801dafd 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 boolean dispatchKeyEvent(KeyEvent event) { 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 4dc463ff81c..e067789bc41 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 00000000000..c30d2ac854a --- /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 00000000000..12fc40e817a --- /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 f98e3dfe845..83a323ec626 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 cf1c6f4e5ad..a482022a177 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 Format copyWithBitrate(int bitrate) { 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 c30d2ac854a..f633bfbf8e1 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 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.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 static PlaybackStats merge(PlaybackStats... 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 static PlaybackStats merge(PlaybackStats... 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 static PlaybackStats merge(PlaybackStats... 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 static PlaybackStats merge(PlaybackStats... 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 static PlaybackStats merge(PlaybackStats... 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 static PlaybackStats merge(PlaybackStats... 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 static PlaybackStats merge(PlaybackStats... 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 int getPlaybackStateAtTime(long realtimeMs) { 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 float getRebufferRate() { 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 12fc40e817a..e7410668e2d 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 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.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 interface Callback { @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 PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) { finishedPlaybackStats = PlaybackStats.EMPTY; playWhenReady = false; playbackState = Player.STATE_IDLE; + playbackSpeed = 1f; period = new Period(); sessionManager.setListener(this); } @@ -158,6 +169,7 @@ public void onSessionCreated(EventTime eventTime, String session) { PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); tracker.onPlayerStateChanged( eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); playbackStatsTrackers.put(session, tracker); sessionStartEventTimes.put(session, eventTime); } @@ -286,6 +298,27 @@ public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { } } + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + playbackSpeed = playbackParameters.speed; + sessionManager.updateSessions(eventTime); + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + } + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); + } + } + } + @Override public void onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { @@ -297,6 +330,88 @@ public void onLoadStarted( } } + @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 @@ private static final class PlaybackStatsTracker { 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 @@ private static final class PlaybackStatsTracker { 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 @@ private static final class PlaybackStatsTracker { 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 PlaybackStatsTracker(boolean keepHistory, EventTime startTime) { this.keepHistory = keepHistory; playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; currentPlaybackStateStartTimeMs = startTime.realtimeMs; playerPlaybackState = Player.STATE_IDLE; firstReportedTimeMs = C.TIME_UNSET; maxRebufferTimeMs = C.TIME_UNSET; isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); + initialAudioFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatHeight = C.LENGTH_UNSET; + currentPlaybackSpeed = 1f; } /** @@ -407,6 +556,10 @@ public void onSeekProcessed(EventTime eventTime) { * @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 void onSuspended(EventTime eventTime, boolean belongsToPlayback) { 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 void onSuspended(EventTime eventTime, boolean belongsToPlayback) { */ 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 PlaybackStats build(boolean isFinal) { 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 PlaybackStats build(boolean isFinal) { /* playbackCount= */ 1, playbackStateDurationsMs, isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + mediaTimeHistory, firstReportedTimeMs, /* foregroundPlaybackCount= */ isForeground ? 1 : 0, /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, @@ -484,7 +754,30 @@ public PlaybackStats build(boolean isFinal) { 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 @@ private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlay 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 @@ private void maybeUpdateMaxRebufferTimeMs(long nowMs) { } } + 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 73e8ac4f3ef..39429a8fa11 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 void runTest( 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 83a323ec626..b6cbe3d2756 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 5b066b79301..47470c5de75 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 @@ private static long readAndDiscard( 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 @@ private static long readAndDiscard( } 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 b6cbe3d2756..cb63af693d0 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 08f4d3a9284..ca8a0b12f97 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 @@ private static double[][] getLogArrayValues(long[][] values) { 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 @@ private static double[][] getSwitchPoints(double[][] logBitrates) { 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 5871371d765..f5d92e2a159 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 @@ private void setDecoderDrmSession(@Nullable DrmSession session) * @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 @@ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackExceptio 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 15532279888..08e8203fd43 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 @@ private void setDecoderDrmSession(@Nullable DrmSession session) decoderDrmSession = session; } + @SuppressWarnings("unchecked") private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = inputFormat; inputFormat = newFormat; @@ -667,12 +668,16 @@ private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException 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 05f83109e83..6d0f9a4aad0 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 @@ protected void onCodecInitialized(String name, long initializedTimestampMs, * @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 @@ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybac 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 14268313eba..2ba3b45ca05 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 HlsTrackMetadataEntry( 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 cb63af693d0..3be8c4ed502 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 6e4cd58d096..3fe22a27626 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 a90435227bb..db3f3943e10 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 3be8c4ed502..f284376cfb7 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 69bb99451ed..6e20db7bf70 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 interface EventListener { @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 long open(DataSpec dataSpec) throws IOException { 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 void close() throws IOException { uri = null; actualUri = null; httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; notifyBytesRead(); try { closeCurrentSource(); @@ -393,7 +396,7 @@ private void openNextSource(boolean checkCache) throws IOException { 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 @@ private void openNextSource(boolean checkCache) throws IOException { } } 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 390deac9333..8f15fb8789d 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.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 CastPlayer(CastContext castContext) { 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 Looper getApplicationLooper() { @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 void seekTo(int windowIndex, long positionMs) { 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 void updateInternalState() { || 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 @@ private void maybeUpdateTimelineAndNotify() { @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 void onSessionResuming(CastSession castSession, String s) { } - // 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 void onResult(@NonNull MediaChannelResult result) { 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 f284376cfb7..a345976c342 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 fe8e898b06b..17591a585ea 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 @@ protected boolean processOutputBuffer( int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (codecNeedsEosBufferTimestampWorkaround @@ -707,7 +708,7 @@ protected boolean processOutputBuffer( 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 6d0f9a4aad0..d6364673037 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 @@ private static String buildCustomDiagnosticInfo(int errorCode) { 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 @@ protected void releaseCodec() { 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 @@ protected boolean flushOrReleaseCodec() { 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 @@ private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exce codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForFirstSyncSample = true; decoderCounters.decoderInitCount++; @@ -1010,6 +1018,11 @@ private boolean feedInputBuffer() throws ExoPlaybackException { 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 @@ private boolean feedInputBuffer() throws ExoPlaybackException { formatQueue.add(presentationTimeUs, inputFormat); waitingForFirstSampleInFormat = false; } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); onQueueInputBuffer(buffer); @@ -1456,7 +1471,9 @@ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) 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 @@ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } catch (IllegalStateException e) { processEndOfStream(); @@ -1492,7 +1510,8 @@ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } @@ -1559,7 +1578,9 @@ private void processOutputBuffersChanged() { * @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 @@ protected abstract boolean processOutputBuffer( int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException; @@ -1652,7 +1674,7 @@ private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackExceptio 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 dbf5f8aa5d2..a56d14083e0 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 @@ private void startLoading() { 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 7193c4c22bf..33eb1095c31 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 @@ protected boolean processOutputBuffer( 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 @@ protected boolean processOutputBuffer( long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; - if (shouldSkip) { + if (isDecodeOnlyBuffer && !isLastBuffer) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -769,10 +770,10 @@ && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeU 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 @@ private void notifyFrameMetadataListener( /** * 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 @@ protected void onProcessedOutputBuffer(long presentationTimeUs) { * 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 @@ protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { * 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 6bd4c8dd14d..e1243d34ba0 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 @@ protected boolean processOutputBuffer( 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 @@ protected boolean processOutputBuffer( 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 a345976c342..e03a0d2dc95 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 00000000000..8cc52c5f101 --- /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 00000000000..457af80a8de --- /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 00000000000..8545787064f --- /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 00000000000..bd9c85da511 --- /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 00000000000..1ddf5c15175 --- /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 00000000000..ce520e70e40 --- /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 00000000000..08feccb3985 --- /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 2c912c17f2e..38fa3a36e50 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 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 d4530d67b74..50fdb68f30f 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 e03a0d2dc95..5128abba465 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 8f15fb8789d..db6f71286ee 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 void updateInternalState() { 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 void updateInternalState() { new ListenerNotificationTask( listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } - maybeUpdateTimelineAndNotify(); flushNotifications(); } @@ -715,16 +724,6 @@ private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) { } } - /** - * 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 a837bd77e57..bc38cbdb8af 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 @@ private final Listener listener; private final ConcatenatingMediaSource concatenatingMediaSource; - private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -271,9 +270,6 @@ public void onPositionDiscontinuity(@DiscontinuityReason int reason) { 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 @@ private void setCurrentPlayer(Player currentPlayer) { this.currentPlayer = currentPlayer; // Media queue management. - castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { exoPlayer.prepare(concatenatingMediaSource); } @@ -355,12 +350,11 @@ private void setCurrentPlayer(Player currentPlayer) { */ 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 d6364673037..d00b218c387 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 @@ protected abstract List getDecoderInfos( * @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 33eb1095c31..45ab06db450 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 @@ protected void configureCodec( MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException { + float codecOperatingRate) { codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( @@ -1210,11 +1209,9 @@ protected MediaFormat getMediaFormat( * @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 @@ protected CodecMaxValues getCodecMaxValues( } /** - * 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 @@ private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) 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 e1243d34ba0..d6b72048a12 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.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 @@ protected void configureCodec( 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 b5f8f954bbb..49aacd9638e 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 @@ private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + timestampMs = SystemClock.elapsedRealtime(); } private ExoPlaybackException(@Type int type, String message) { @@ -138,6 +143,7 @@ private ExoPlaybackException(@Type int type, String message) { 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 2877b2a1cc9..02b2990193c 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.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 void getNextChunk( 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 64ec1adb434..37230696f88 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 @@ protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long } 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 @@ protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, language, roleDescriptors, accessibilityDescriptors, - segmentBase); + segmentBase, + supplementalProperties); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -332,7 +333,7 @@ protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, } 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 @@ protected RepresentationInfo parseRepresentation( 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 @@ protected RepresentationInfo parseRepresentation( } 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 @@ protected SegmentList buildSegmentList( 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 @@ protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplat } return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + startNumber, duration, timeline, initializationTemplate, mediaTemplate, + parentSupplementalProperties); } protected SegmentTemplate buildSegmentTemplate( @@ -799,9 +804,21 @@ protected SegmentTemplate buildSegmentTemplate( 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 f0332325909..720c20eed2c 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 static class MultiSegmentBase extends 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 MultiSegmentBase( 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 SegmentTemplate( 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 RangedUri getSegmentUrl(Representation representation, long sequenceNumbe @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 bfc9a378441..aeaa977b120 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 void addTransferListener(TransferListener transferListener) { maybeAddListenerToDataSource(assetDataSource, transferListener); maybeAddListenerToDataSource(contentDataSource, transferListener); maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(udpDataSource, transferListener); maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @@ -161,6 +164,8 @@ public long open(DataSpec dataSpec) throws IOException { 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 void close() throws IOException { } } + 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 b5f8f954bbb..49aacd9638e 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 @@ private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + timestampMs = SystemClock.elapsedRealtime(); } private ExoPlaybackException(@Type int type, String message) { @@ -138,6 +143,7 @@ private ExoPlaybackException(@Type int type, String message) { 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 a1ee80767d3..7e30d924a06 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.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 int read(byte[] buffer, int offset, int readLength) throws 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 int read(byte[] buffer, int offset, int readLength) throws 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 @@ private void resetConnectTimeout() { 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 @@ private static boolean isEmpty(@Nullable List list) { return list == null || list.isEmpty(); } + // Copy as much as possible from the src buffer into dst buffer. + // Returns the number of bytes copied. + private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) { + int remaining = Math.min(src.remaining(), dst.remaining()); + int limit = src.limit(); + src.limit(src.position() + remaining); + dst.put(src); + src.limit(limit); + return remaining; + } + private final class UrlRequestCallback extends UrlRequest.Callback { @Override 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 a01c5e84b64..2be369bad94 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 void testOverread() throws HttpDataSourceException { 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 void testReadFailure() throws HttpDataSourceException { } } + @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 void run() { 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 @@ private static ByteBuffer buildTestDataBuffer(int position, int length) { 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 45ab06db450..f60dbf3cb73 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 @@ protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { 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 5128abba465..bc9f64a0017 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 82fb8bb9f52..929b579b4c6 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 Pair getErrorMessage(ExoPlaybackException e) { // 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 Pair getErrorMessage(ExoPlaybackException e) { 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 d00b218c387..4b7bab2cfaa 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 static class DecoderInitializationException extends Exception { 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 DecoderInitializationException(Format format, Throwable cause, 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 @@ private DecoderInitializationException( 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 @@ private DecoderInitializationException copyWithFallbackException( getCause(), mimeType, secureDecoderRequired, - decoderName, + codecInfo, diagnosticInfo, fallbackException); } @@ -159,9 +161,34 @@ private static String getDiagnosticInfoV21(Throwable cause) { 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 @@ protected void onStopped() { @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 @@ protected boolean flushOrReleaseCodec() { 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 @@ private void maybeInitCodecWithFallback( 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 @@ private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( 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 f60dbf3cb73..c864adfa68f 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 @@ protected CodecMaxValues getCodecMaxValues( 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 68322c60a11..74d8ddad3d0 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 BaseMediaChunk( 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 6b7f5688ae7..d6400c51658 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 d9b28d9c92e..499aea6a0c1 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 interface ReleaseCallback { 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 interface ReleaseCallback { @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 ChunkSampleStream( */ 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 9626f4b03f4..39c097826f3 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 MediaChunk( 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 168783cf1c0..375faff797f 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 bfc9a378441..aeaa977b120 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 void addTransferListener(TransferListener transferListener) { maybeAddListenerToDataSource(assetDataSource, transferListener); maybeAddListenerToDataSource(contentDataSource, transferListener); maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(udpDataSource, transferListener); maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @@ -161,6 +164,8 @@ public long open(DataSpec dataSpec) throws IOException { 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 void close() throws IOException { } } + 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 3900dc8e938..2110ac2c489 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 @@ protected DownloadService( 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 static void startForeground(Context context, 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 4cd03f566d4..910a8efbe97 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 aa9e4b14922..0c34cc7bba9 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 static PlayerNotificationManager createWithNotificationChannel( 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 static PlayerNotificationManager createWithNotificationChannel( 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 66731660f57..7b7c539aeea 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 @@ private void parseStreamElementStartTag(XmlPullParser parser) throws ParserExcep } 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 @@ private static class QualityLevelParser extends ElementParser { 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 void parseStartTag(XmlPullParser parser) throws ParserException { 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 void parseStartTag(XmlPullParser parser) throws ParserException { /* 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 84e984445a2..4e18df04e3a 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 @@ private class MediaDrmEventListener implements OnEventListener { @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 49915f3af55..6bd8d9688f9 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 @@ interface OnEventListener { */ void onEvent( ExoMediaDrm mediaDrm, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data); @@ -215,6 +215,7 @@ KeyRequest getKeyRequest( 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 848d9e146a1..609abd4e1e9 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 @@ private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException { } } - // FIXME: incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") @Override public void setOnEventListener( final ExoMediaDrm.OnEventListener listener) { @@ -160,8 +158,7 @@ public KeyRequest getKeyRequest( return new KeyRequest(requestData, licenseServerUrl); } - // FIXME: incompatible types in return. - @SuppressWarnings("nullness:return.type.incompatible") + @Nullable @Override public byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException { 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 e6679e1a5aa..752239c991c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.os.PersistableBundle; import androidx.annotation.RequiresPermission; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -129,9 +130,8 @@ public boolean onStartJob(JobParameters params) { logd("Requirements are met"); String serviceAction = extras.getString(KEY_SERVICE_ACTION); String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); - // FIXME: incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") - Intent intent = new Intent(serviceAction).setPackage(servicePackage); + Intent intent = + new Intent(Assertions.checkNotNull(serviceAction)).setPackage(servicePackage); logd("Starting service action: " + serviceAction + " package: " + servicePackage); Util.startForegroundService(this, intent); } else { From b449269552537b2cb0a8b46e29cccc7c42c0807d Mon Sep 17 00:00:00 2001 From: andrewlewis 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 7e30d924a06..2cd40c8d70c 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.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.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 @@ private void readInternal(ByteBuffer buffer) throws HttpDataSourceException { } 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 @@ private void readInternal(ByteBuffer buffer) throws HttpDataSourceException { } 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 7c2c8a6e0b1..3c34d9b577c 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 void setAdWillPlayMuted(boolean b) { 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 a4996278bd1..c0980df4401 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 ff29f3a7d1c..c8d4668a6ad 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 00000000000..5645d707878 --- /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 00000000000..e29f74148c8 --- /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 00000000000..2d190bdef4b --- /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 00000000000..71b05eb2c18 --- /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 00000000000..dfc9b4e0e5e --- /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 00000000000..64de3e8a8e7 --- /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 00000000000..f70695d8739 --- /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 00000000000..37c6fd41eb9 --- /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 00000000000..151ac87fbef --- /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 00000000000..5e67219e756 --- /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 00000000000..23e2708f8e9 --- /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 00000000000..d2039a51529 --- /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 00000000000..d6544a6f377 --- /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 00000000000..0af36d7b8c4 --- /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 00000000000..819e52a755f --- /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 00000000000..e9600429f00 --- /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 00000000000..22e2b8ded5c --- /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 00000000000..d7ffc58f4c2 --- /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 00000000000..e5df5ec2f47 --- /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 00000000000..75afd9e5d39 --- /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 00000000000..721360e8a7b --- /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 00000000000..af9254c59ef --- /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 00000000000..a90a367691b --- /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 00000000000..3e7daaf5737 --- /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 00000000000..244ac72829e --- /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 00000000000..87cefe1884b --- /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 00000000000..96dfbf86141 --- /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 00000000000..b46361fb2ed --- /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 00000000000..303a1caf648 --- /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 00000000000..a7dafd31767 --- /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 00000000000..22244675b79 --- /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 00000000000..8e58185cfa0 --- /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 00000000000..e8ad2c1a0db --- /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 00000000000..7c1f06e8d2b --- /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 00000000000..c08a9bc352c --- /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 00000000000..36173bfc5dd --- /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 00000000000..1529e9f5ac9 --- /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 00000000000..56b5d3cc8ca --- /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 00000000000..e24970ba0d1 --- /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 00000000000..115536ac4cc --- /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 00000000000..cb5eff4f374 --- /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 00000000000..8cb60563402 --- /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 00000000000..b900a78937f --- /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 00000000000..58f78b090a1 --- /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 00000000000..f6084339e45 --- /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 00000000000..fbe936a0166 --- /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 cb548ec3fd2..12db27d68ec 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 void release() { * @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 @@ private void onFormat(Format format) { 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 b2c09bd70f0..542565e70d1 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.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 @@ public synchronized void rewind() { 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 976a5d4e48f..921afcdf2f4 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.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 interface UpstreamFormatChangedListener { } + /** 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 boolean setReadPosition(int sampleIndex) { 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 ef3cb0bbe3a..30083cb8498 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 @@ private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( } 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 e73fdd58a35..c031fcde21f 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 final MediaPeriod createPeriod( holder = new MediaSourceHolder(new DummyMediaSource()); holder.hasStartedPreparing = true; } - DeferredMediaPeriod mediaPeriod = - new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs); + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(holder.mediaSource, id, allocator, startPositionUs); mediaSourceByMediaPeriod.put(mediaPeriod, holder); holder.activeMediaPeriods.add(mediaPeriod); if (!holder.hasStartedPreparing) { @@ -474,7 +474,7 @@ public final MediaPeriod createPeriod( public final void releasePeriod(MediaPeriod mediaPeriod) { MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); - ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); holder.activeMediaPeriods.remove(mediaPeriod); maybeReleaseChildSource(holder); } @@ -784,7 +784,7 @@ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Time // 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. Assertions.checkState(mediaSourceHolder.activeMediaPeriods.size() <= 1); - DeferredMediaPeriod deferredMediaPeriod = + MaskingMediaPeriod deferredMediaPeriod = mediaSourceHolder.activeMediaPeriods.isEmpty() ? null : mediaSourceHolder.activeMediaPeriods.get(0); @@ -897,7 +897,7 @@ private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodU public final MediaSource mediaSource; public final Object uid; - public final List 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 95a218bfe77..344c4989ebd 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 @@ * #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 interface PrepareErrorListener { /** 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 interface PrepareErrorListener { 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 void setPrepareErrorListener(PrepareErrorListener listener) { 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 78b0f6de11e..dd4c0d26b22 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 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 RuntimeException getRuntimeExceptionForUnexpected() { 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 AdsMediaSource( 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star @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 void releaseSourceInternal() { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; - deferredMediaPeriodByAdMediaSource.clear(); + maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; contentManifest = null; adPlaybackState = null; @@ -319,11 +319,11 @@ private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, 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 void onAdLoadError(final AdLoadException error, DataSpec dataSpec) { } } - 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 da66f3dd105..1c6458c5516 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 @@ private void verifyApplicationThread() { 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 8cb60563402..c1b12428d44 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 38fa3a36e50..e22c97859ae 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 4ac7ad6506c..183a74544dd 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 f633bfbf8e1..ed127bc5509 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 long getPlaybackStateDurationMs(@PlaybackState int playbackState) { * @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 8c581337043..6444b4747f8 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 @@ private static final class PlaybackStatsTracker { 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 void onNonFatalError(EventTime eventTime, Exception error) { * * @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 PlaybackStats build(boolean isFinal) { : 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 PlaybackStats build(boolean isFinal) { 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 @@ private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlay } } - @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 12db27d68ec..4f0c5b87aa5 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 a9f9da0a952..2ba6ab4c696 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 @@ */ 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 b2333516a81..4145d9a1c7f 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 @@ private long getInitialBitrateEstimateForNetworkType(@C.NetworkType int networkT */ 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 2a8b393ed3a..2488ae0ff3c 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 @@ 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 1d4481b5cce..ea37612c887 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 c55cf311495..02ed0a534ec 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 interface TrackSelectionListener { 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 3d7e57bbd24..6ef9d4907d0 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 8a211d0879e..9a8c787e779 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 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 499aea6a0c1..efc3b475968 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.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 interface ReleaseCallback { /* 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 99a3d271bdf..c2007b19a39 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 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 d0f9e2ae049..ee29af9c99c 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 @@ */ 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 ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object mes */ 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 c458f22050b..945bd32d303 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.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 @@ public void release() { /* 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 @@ public PlayerMessage createMessage(Target target) { 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 1c6458c5516..b427991d6ee 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 void release() { 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 56de0a8b337..df96b634ddc 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 PlayerMessage createMessage(PlayerMessage.Target target) { 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 3de52b560c8..80fecf19ccc 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 CacheDataSink(Cache cache, long fragmentSize, int bufferSize) { 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 @@ private void closeCurrentOutputStream() throws IOException { 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 856e9db168e..ce9735badd6 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 CacheDataSinkFactory(Cache cache, long fragmentSize, int bufferSize) { 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 a72c70442e5..a7754816338 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 @@ */ 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 bdf127be59d..1622d68d991 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 @@ */ 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 c031fcde21f..c2ec437d84c 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 @@ * 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 ConcatenatingMediaSource( * * @param mediaSource The {@link MediaSource} to be added to the list. */ - public final synchronized void addMediaSource(MediaSource mediaSource) { + public synchronized void addMediaSource(MediaSource mediaSource) { addMediaSource(mediaSourcesPublic.size(), mediaSource); } @@ -161,7 +161,7 @@ public final synchronized void addMediaSource(MediaSource mediaSource) { * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * source has been added to the playlist. */ - public final synchronized void addMediaSource( + public synchronized void addMediaSource( MediaSource mediaSource, Handler handler, Runnable onCompletionAction) { addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction); } @@ -173,7 +173,7 @@ public final synchronized void addMediaSource( * be in the range of 0 <= index <= {@link #getSize()}. * @param mediaSource The {@link MediaSource} to be added to the list. */ - public final synchronized void addMediaSource(int index, MediaSource mediaSource) { + public synchronized void addMediaSource(int index, MediaSource mediaSource) { addPublicMediaSources( index, Collections.singletonList(mediaSource), @@ -191,7 +191,7 @@ public final synchronized void addMediaSource(int index, MediaSource mediaSource * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * source has been added to the playlist. */ - public final synchronized void addMediaSource( + public synchronized void addMediaSource( int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) { addPublicMediaSources( index, Collections.singletonList(mediaSource), handler, onCompletionAction); @@ -203,7 +203,7 @@ public final synchronized void addMediaSource( * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * sources are added in the order in which they appear in this collection. */ - public final synchronized void addMediaSources(Collection mediaSources) { + public synchronized void addMediaSources(Collection mediaSources) { addPublicMediaSources( mediaSourcesPublic.size(), mediaSources, @@ -221,7 +221,7 @@ public final synchronized void addMediaSources(Collection mediaSour * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * sources have been added to the playlist. */ - public final synchronized void addMediaSources( + public synchronized void addMediaSources( Collection mediaSources, Handler handler, Runnable onCompletionAction) { addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } @@ -234,7 +234,7 @@ public final synchronized void addMediaSources( * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * sources are added in the order in which they appear in this collection. */ - public final synchronized void addMediaSources(int index, Collection mediaSources) { + public synchronized void addMediaSources(int index, Collection mediaSources) { addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } @@ -249,7 +249,7 @@ public final synchronized void addMediaSources(int index, Collection mediaSources, Handler handler, @@ -269,7 +269,7 @@ public final synchronized void addMediaSources( * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. */ - public final synchronized void removeMediaSource(int index) { + public synchronized void removeMediaSource(int index) { removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null); } @@ -288,7 +288,7 @@ public final synchronized void removeMediaSource(int index) { * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * source has been removed from the playlist. */ - public final synchronized void removeMediaSource( + public synchronized void removeMediaSource( int index, Handler handler, Runnable onCompletionAction) { removePublicMediaSources(index, index + 1, handler, onCompletionAction); } @@ -307,7 +307,7 @@ public final synchronized void removeMediaSource( * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} < 0, * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} */ - public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { + public synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { removePublicMediaSources( fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null); } @@ -329,7 +329,7 @@ public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} */ - public final synchronized void removeMediaSourceRange( + public synchronized void removeMediaSourceRange( int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) { removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction); } @@ -342,7 +342,7 @@ public final synchronized void removeMediaSourceRange( * @param newIndex The target index of the media source in the playlist. This index must be in the * range of 0 <= index < {@link #getSize()}. */ - public final synchronized void moveMediaSource(int currentIndex, int newIndex) { + public synchronized void moveMediaSource(int currentIndex, int newIndex) { movePublicMediaSource( currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null); } @@ -359,13 +359,13 @@ public final synchronized void moveMediaSource(int currentIndex, int newIndex) { * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * source has been moved. */ - public final synchronized void moveMediaSource( + public synchronized void moveMediaSource( int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) { movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction); } /** Clears the playlist. */ - public final synchronized void clear() { + public synchronized void clear() { removeMediaSourceRange(0, getSize()); } @@ -376,12 +376,12 @@ public final synchronized void clear() { * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist * has been cleared. */ - public final synchronized void clear(Handler handler, Runnable onCompletionAction) { + public synchronized void clear(Handler handler, Runnable onCompletionAction) { removeMediaSourceRange(0, getSize(), handler, onCompletionAction); } /** Returns the number of media sources in the playlist. */ - public final synchronized int getSize() { + public synchronized int getSize() { return mediaSourcesPublic.size(); } @@ -391,7 +391,7 @@ public final synchronized int getSize() { * @param index An index in the range of 0 <= index <= {@link #getSize()}. * @return The {@link MediaSource} at this index. */ - public final synchronized MediaSource getMediaSource(int index) { + public synchronized MediaSource getMediaSource(int index) { return mediaSourcesPublic.get(index).mediaSource; } @@ -400,7 +400,7 @@ public final synchronized MediaSource getMediaSource(int index) { * * @param shuffleOrder A {@link ShuffleOrder}. */ - public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { + public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null); } @@ -412,7 +412,7 @@ public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle * order has been changed. */ - public final synchronized void setShuffleOrder( + public synchronized void setShuffleOrder( ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) { setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction); } @@ -426,8 +426,7 @@ public Object getTag() { } @Override - public final synchronized void prepareSourceInternal( - @Nullable TransferListener mediaTransferListener) { + public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); if (mediaSourcesPublic.isEmpty()) { @@ -447,8 +446,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public final MediaPeriod createPeriod( - MediaPeriodId id, Allocator allocator, long startPositionUs) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); if (holder == null) { @@ -471,7 +469,7 @@ public final MediaPeriod createPeriod( } @Override - public final void releasePeriod(MediaPeriod mediaPeriod) { + public void releasePeriod(MediaPeriod mediaPeriod) { MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); @@ -480,7 +478,7 @@ public final void releasePeriod(MediaPeriod mediaPeriod) { } @Override - public final synchronized void releaseSourceInternal() { + public synchronized void releaseSourceInternal() { super.releaseSourceInternal(); mediaSourceHolders.clear(); mediaSourceByUid.clear(); @@ -497,7 +495,7 @@ public final synchronized void releaseSourceInternal() { } @Override - protected final void onChildSourceInfoRefreshed( + protected void onChildSourceInfoRefreshed( MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline, @@ -506,7 +504,8 @@ protected final void onChildSourceInfoRefreshed( } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) { for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) { // Ensure the reported media period id has the same window sequence number as the one created diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java deleted file mode 100644 index 1f77cae20d3..00000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source; - -/** @deprecated Use {@link ConcatenatingMediaSource} instead. */ -@Deprecated -public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource { - - /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(MediaSource...)} - * instead. - */ - @Deprecated - public DynamicConcatenatingMediaSource() {} - - /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, - * MediaSource...)} instead. - */ - @Deprecated - public DynamicConcatenatingMediaSource(boolean isAtomic) { - super(isAtomic); - } - - /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, ShuffleOrder, - * MediaSource...)} instead. - */ - @Deprecated - public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { - super(isAtomic, shuffleOrder); - } -} From 2a765f6b5a51144a171e5e2bfcb93f95ad638330 Mon Sep 17 00:00:00 2001 From: tonihei 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 c3e700fff58..c942f9320e7 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 Object getTag() { } @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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 06db088f06e..1a9e1ff250c 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 @@ protected CompositeMediaSource() { @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 void maybeThrowSourceInfoRefreshError() throws IOException { @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 d9003e443ec..f07ee63e790 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 Object getTag() { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); } @@ -359,7 +359,7 @@ public void releasePeriod(MediaPeriod mediaPeriod) { } @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 769f545aaa6..7adb18dc946 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 Object getTag() { } @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 7188cada0f0..f12ce92f54b 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 Object getTag() { } @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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 5ed12154b32..ba69b46d7f1 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 Object getTag() { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); } @@ -262,7 +262,7 @@ public void releasePeriod(MediaPeriod mediaPeriod) { } @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 b03dd0ea7c8..fc99e8cb7bf 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 SilenceMediaSource(long durationUs) { } @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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 55d967cd691..6c1881a01a6 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 Object getTag() { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; refreshSourceInfo(timeline, /* manifest= */ null); } @@ -331,7 +331,7 @@ public void releasePeriod(MediaPeriod mediaPeriod) { } @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 dd4c0d26b22..a6c2cf27673 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 Object getTag() { } @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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 779a97fd09c..551555502fd 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 Object getTag() { } @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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 fbb6285d1d5..f891670e780 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 Object getTag() { } @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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 3c18bfe6445..e31fbccae50 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 Object getTag() { } @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 void releasePeriod(MediaPeriod period) { } @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 0d50f22bc09..80456169ffd 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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 e49602957fe..c83214c8d53 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 @@ /** 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 1564157d441..e7f2c1935bf 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 H262Reader() { 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 895c2246976..d032ef58837 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.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 df5b19c0527..22ce893fce9 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 DvbDecoder(List initializationData) { } @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 c25b26128c5..d701f99d734 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 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 SsaDecoder(List initializationData) { } @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 6f9fd366ec7..cf174283ec7 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.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 @@ 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 SubripDecoder() { } @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 6e0c495466c..6dabcdd9048 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 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 TtmlDecoder() { } @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 b977f61a8a4..8b255ac2bdc 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 @@ 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 Mp4WebvttDecoder() { } @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 fe3c86bd1ee..9b356f09885 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 @@ 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 WebvttDecoder() { } @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 ea37612c887..81212b731f0 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 synchronized long getCacheSpace() { } @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 synchronized SimpleCacheSpan startReadWrite(String key, long position) @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 ab67ac115b6..70959628018 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 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 void testDecodeEmpty() throws IOException { 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 void testDecodeTypicalWithInitializationData() throws IOException { 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 void testDecodeInvalidTimecodes() throws IOException { 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 void testDecodeNoEndTimecodes() throws IOException { 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 void testDecodeNoEndTimecodes() throws IOException { .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 9520262207e..774e8d98b94 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.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 void testDecodeEmpty() throws IOException { 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 void testDecodeTypicalWithByteOrderMark() throws IOException { 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 void testDecodeTypicalExtraBlankLine() throws IOException { 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 void testDecodeTypicalMissingTimecode() throws IOException { 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 void testDecodeTypicalMissingSequence() throws IOException { 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 void testDecodeTypicalNegativeTimestamps() throws IOException { 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 void testDecodeTypicalUnexpectedEnd() throws IOException { 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 void testDecodeNoEndTimecodes() throws IOException { 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 void testDecodeCueWithTag() throws IOException { 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 void testDecodeCueWithTag() throws IOException { 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 @@ private static void assertTypicalCue3(SubripSubtitle subtitle, int eventIndex) { } 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 85af6482c0e..22c7288340c 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 @@ private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) { 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 2a7289c0392..9320a3f31c0 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 @@ private WebvttSubtitle getSubtitleForTestAsset(String asset) 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 956a5fc2839..83104119ad0 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 void testSwitchToCacheSourceWithNonBlockingCacheDataSource() throws Excep .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 3d684aab822..fc229d9dc6a 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 void testSetGetContentMetadata() throws Exception { .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 9ec3886df55..eaebf8b4e1f 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 void onPlay() { } } 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 e79c776f88f..3310b0dc8b4 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 @@ private static boolean isSecureV21(CodecCapabilities capabilities) { @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 7e01801dafd..269c48c282a 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 SubtitleView getSubtitleView() { @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 View[] getAdOverlayViews() { // Internal methods. private boolean toggleControllerVisibility() { - if (!useController || player == null) { - return false; - } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1471,6 +1471,9 @@ public void onLayoutChange( @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 c2ec437d84c..bdf55fe40db 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 @@ 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 synchronized void releaseSourceInternal() { mediaSourceHolders.clear(); mediaSourceByUid.clear(); shuffleOrder = shuffleOrder.cloneAndClear(); - windowCount = 0; - periodCount = 0; if (playbackThreadHandler != null) { playbackThreadHandler.removeCallbacksAndMessages(null); playbackThreadHandler = null; @@ -702,9 +697,7 @@ private void updateTimelineAndScheduleOnCompletionActions() { Set 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 @@ private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSour MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); newMediaSourceHolder.reset( newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), - previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); + previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount()); } else { - newMediaSourceHolder.reset( - newIndex, /* firstWindowIndexInChild= */ 0, /* firstPeriodIndexInChild= */ 0); + newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); } correctOffsets( - newIndex, - /* childIndexUpdate= */ 1, - newMediaSourceHolder.timeline.getWindowCount(), - newMediaSourceHolder.timeline.getPeriodCount()); + newIndex, /* childIndexUpdate= */ 1, newMediaSourceHolder.timeline.getWindowCount()); mediaSourceHolders.add(newIndex, newMediaSourceHolder); mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); if (!useLazyPreparation) { @@ -764,14 +752,15 @@ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Time if (deferredTimeline.getTimeline() == timeline) { return; } - int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount(); - int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount(); - if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) { - correctOffsets( - mediaSourceHolder.childIndex + 1, - /* childIndexUpdate= */ 0, - windowOffsetUpdate, - periodOffsetUpdate); + if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { + MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); + int windowOffsetUpdate = + timeline.getWindowCount() + - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild); + if (windowOffsetUpdate != 0) { + correctOffsets( + mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate); + } } if (mediaSourceHolder.isPrepared) { mediaSourceHolder.timeline = deferredTimeline.cloneWithUpdatedTimeline(timeline); @@ -828,11 +817,7 @@ private void removeMediaSourceInternal(int index) { MediaSourceHolder holder = mediaSourceHolders.remove(index); mediaSourceByUid.remove(holder.uid); Timeline oldTimeline = holder.timeline; - correctOffsets( - index, - /* childIndexUpdate= */ -1, - -oldTimeline.getWindowCount(), - -oldTimeline.getPeriodCount()); + correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); holder.isRemoved = true; maybeReleaseChildSource(holder); } @@ -841,25 +826,22 @@ private void moveMediaSourceInternal(int currentIndex, int newIndex) { int startIndex = Math.min(currentIndex, newIndex); int endIndex = Math.max(currentIndex, newIndex); int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; - int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild; mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex)); for (int i = startIndex; i <= endIndex; i++) { MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.childIndex = i; holder.firstWindowIndexInChild = windowOffset; - holder.firstPeriodIndexInChild = periodOffset; windowOffset += holder.timeline.getWindowCount(); - periodOffset += holder.timeline.getPeriodCount(); } } - private void correctOffsets( - int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) { - windowCount += windowOffsetUpdate; - periodCount += periodOffsetUpdate; + private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffsetUpdate) { + // TODO: Replace window index with uid in reporting to get rid of this inefficient method and + // the childIndex and firstWindowIndexInChild variables. for (int i = startIndex; i < mediaSourceHolders.size(); i++) { - mediaSourceHolders.get(i).childIndex += childIndexUpdate; - mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; - mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; + MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.childIndex += childIndexUpdate; + holder.firstWindowIndexInChild += windowOffsetUpdate; } } @@ -892,7 +874,7 @@ private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodU } /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder implements Comparable { + /* package */ static final class MediaSourceHolder { public final MediaSource mediaSource; public final Object uid; @@ -901,7 +883,6 @@ private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodU public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - public int firstPeriodIndexInChild; public boolean hasStartedPreparing; public boolean isPrepared; public boolean isRemoved; @@ -913,20 +894,14 @@ public MediaSourceHolder(MediaSource mediaSource) { this.uid = new Object(); } - public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) { + public void reset(int childIndex, int firstWindowIndexInChild) { this.childIndex = childIndex; this.firstWindowIndexInChild = firstWindowIndexInChild; - this.firstPeriodIndexInChild = firstPeriodIndexInChild; this.hasStartedPreparing = false; this.isPrepared = false; this.isRemoved = false; this.activeMediaPeriods.clear(); } - - @Override - public int compareTo(@NonNull MediaSourceHolder other) { - return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; - } } /** Message used to post actions from app thread to playback thread. */ @@ -956,13 +931,9 @@ private static final class ConcatenatedTimeline extends AbstractConcatenatedTime public ConcatenatedTimeline( Collection 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 ConcatenatedTimeline( uids = new Object[childCount]; childIndexByUid = new HashMap<>(); 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 | 416 ++++- 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, 404 insertions(+), 13778 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 2bd0526cdda..00000000000 --- 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 6504cb4f943..00000000000 --- 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 e6be3b9026f..00000000000 --- 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 ff77e1cbfa5..00000000000 --- 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 19a118913b9..00000000000 --- 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 5645d707878..00000000000 --- 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 e29f74148c8..00000000000 --- 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 2d190bdef4b..00000000000 --- 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 71b05eb2c18..00000000000 --- 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 dfc9b4e0e5e..00000000000 --- 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 64de3e8a8e7..00000000000 --- 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 f70695d8739..00000000000 --- 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 37c6fd41eb9..00000000000 --- 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 151ac87fbef..00000000000 --- 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 5e67219e756..00000000000 --- 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 23e2708f8e9..00000000000 --- 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 d2039a51529..00000000000 --- 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 d6544a6f377..00000000000 --- 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 0af36d7b8c4..00000000000 --- 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 819e52a755f..00000000000 --- 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 e9600429f00..00000000000 --- 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 22e2b8ded5c..00000000000 --- 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 d7ffc58f4c2..00000000000 --- 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 e5df5ec2f47..00000000000 --- 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 75afd9e5d39..00000000000 --- 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 721360e8a7b..00000000000 --- 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 af9254c59ef..00000000000 --- 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 a90a367691b..00000000000 --- 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 3e7daaf5737..00000000000 --- 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 244ac72829e..00000000000 --- 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 87cefe1884b..00000000000 --- 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 96dfbf86141..00000000000 --- 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 b46361fb2ed..00000000000 --- 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 303a1caf648..00000000000 --- 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 a7dafd31767..00000000000 --- 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 22244675b79..00000000000 --- 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 8e58185cfa0..00000000000 --- 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 bc38cbdb8af..00000000000 --- 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 9599da15cb7..2d5a5f0ccfd 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 @@ public DrmConfiguration( "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 e8ad2c1a0db..00000000000 --- 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 5ed434eed6f..c17c0a62ab0 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.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 void onResume() { // 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 void onQueuePositionChanged(int previousIndex, int newIndex) { } } - @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 c9a728b3ffd..c92ebd7e94c 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); + } + + 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 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(); + + 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. + */ + 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)); - /** Called when the media queue changes due to modifications not caused by this manager. */ - void onQueueContentsExternallyChanged(); + // 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); + } - /** Called when an error occurs in the current player. */ - void onPlayerError(); + return true; } - /** Redirects the given {@code keyEvent} to the active player. */ - boolean dispatchKeyEvent(KeyEvent keyEvent); + /** + * 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(); + } - /** Appends the given {@link MediaItem} to the media queue. */ - void addItem(MediaItem mediaItem); + // Player.EventListener implementation. - /** Returns the number of items in the media queue. */ - int getMediaQueueSize(); + @Override + public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + updateCurrentItemIndex(); + } - /** Selects the item at the given position for playback. */ - void selectQueueItem(int position); + @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); + } + } /** - * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is - * being played. + * 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. */ - int getCurrentItemIndex(); + 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); + } + } - /** Returns the {@link MediaItem} at the given {@code position}. */ - MediaItem getItem(int position); + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } + } - /** Moves the item at position {@code from} to position {@code to}. */ - boolean moveItem(MediaItem item, int to); + 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); + } + } - /** Removes the item at position {@code index}. */ - boolean removeItem(MediaItem item); + 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(); + } - /** Releases any acquired resources. */ - void release(); + @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 013b50a175c..2f0acd48085 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 7c1f06e8d2b..00000000000 --- 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 5aed1373e5e..8948173f606 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 c08a9bc352c..00000000000 --- 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 36173bfc5dd..00000000000 --- 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 1529e9f5ac9..00000000000 --- 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 56b5d3cc8ca..00000000000 --- 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 e24970ba0d1..00000000000 --- 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 115536ac4cc..00000000000 --- 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 cb5eff4f374..00000000000 --- 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 184e347e1c5..00000000000 --- 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 c1b12428d44..00000000000 --- 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 b900a78937f..00000000000 --- 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 58f78b090a1..00000000000 --- 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 f6084339e45..00000000000 --- 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 fbe936a0166..00000000000 --- 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 eaebf8b4e1f..3136e3cca98 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 interface MediaMetadataProvider { /** * 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 62e985f98b5..d76ca54b7b3 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 ec5ad88aebb..b10f2bf80bc 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 @@ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatEx } @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 @@ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserEx 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 15b36157fb8..f6835558f29 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 FlvExtractor() { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -131,7 +131,7 @@ public void init(ExtractorOutput output) { @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 @@ private boolean readTagHeader(ExtractorInput input) throws IOException, Interrup */ 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 @@ private boolean readTagData(ExtractorInput input) throws IOException, Interrupte 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 @@ private void ensureReadyForMediaOutput() { 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 2dec85ffcc9..eb1cc8f336d 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 @@ protected boolean parseHeader(ParsableByteArray data) { } @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 @@ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserEx 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 @@ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserEx 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 e8652d653f9..48914b7c2c9 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 @@ protected TagPayloadReader(TrackOutput output) { * * @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 @@ public final void consume(ParsableByteArray data, long timeUs) throws ParserExce /** * 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 92db91e20b1..5ddaafb4a89 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 @@ // State variables. private boolean hasOutputFormat; + private boolean hasOutputKeyframe; private int frameType; /** @@ -60,7 +61,7 @@ public VideoTagPayloadReader(TrackOutput output) { @Override public void seek() { - // Do nothing. + hasOutputKeyframe = false; } @Override @@ -77,7 +78,7 @@ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatEx } @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 @@ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserEx 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 @@ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserEx 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 59337c08472..c58293dd456 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 LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eve @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 @@ protected Format getOutputFormat() { 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 285be96388b..cd5d67f686f 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 @@ private 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 static String getVersion() { return isAvailable() ? opusGetVersion() : null; } + /** + * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding + * protected content. + */ + public static boolean matchesExpectedExoMediaCryptoType( + 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 1734a05e864..b4db4971cc2 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 LibvpxVideoRenderer( 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 5a65fc56ff9..2c25f674d66 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 @@ private 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 static boolean isHighBitDepthSupported() { 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 a482022a177..f06c9da048f 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.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 static Format createVideoContainerFormat( /* 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 static Format createVideoSampleFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Audio. @@ -413,7 +426,8 @@ public static Format createAudioContainerFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createAudioSampleFormat( @@ -518,7 +532,8 @@ public static Format createAudioSampleFormat( encoderDelay, encoderPadding, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Text. @@ -585,7 +600,8 @@ public static Format createTextContainerFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } public static Format createTextSampleFormat( @@ -698,7 +714,8 @@ public static Format createTextSampleFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } // Image. @@ -740,7 +757,8 @@ public static Format createImageSampleFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Generic. @@ -804,7 +822,8 @@ public static Format createContainerFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -837,7 +856,8 @@ public static Format createSampleFormat( /* 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 static Format createSampleFormat( /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } /* package */ Format( @@ -910,7 +931,9 @@ public static Format createSampleFormat( 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 static Format createSampleFormat( // 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 static Format createSampleFormat( // 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 Format copyWithMaxInputSize(int maxInputSize) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { @@ -1051,7 +1079,8 @@ public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithContainerInfo( @@ -1099,7 +1128,8 @@ public Format copyWithContainerInfo( encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } @SuppressWarnings("ReferenceEquality") @@ -1178,7 +1208,8 @@ public Format copyWithManifestFormatInfo(Format manifestFormat) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -1210,7 +1241,8 @@ public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithFrameRate(float frameRate) { @@ -1242,7 +1274,8 @@ public Format copyWithFrameRate(float frameRate) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { @@ -1274,7 +1307,8 @@ public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithMetadata(@Nullable Metadata metadata) { @@ -1306,7 +1340,8 @@ public Format copyWithMetadata(@Nullable Metadata metadata) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithRotationDegrees(int rotationDegrees) { @@ -1338,7 +1373,8 @@ public Format copyWithRotationDegrees(int rotationDegrees) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithBitrate(int bitrate) { @@ -1370,7 +1406,8 @@ public Format copyWithBitrate(int bitrate) { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithVideoSize(int width, int height) { @@ -1402,7 +1439,41 @@ public Format copyWithVideoSize(int width, int height) { 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 int hashCode() { // 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 boolean equals(@Nullable Object obj) { && 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 17591a585ea..7e889097bce 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 @@ protected int supportsFormat(MediaCodecSelector mediaCodecSelector, 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 4e18df04e3a..34fd223c645 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa 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 716da7a0ad3..9211cec1447 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 @@ 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 DrmSession acquireSession( 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 DrmSession acquireSession( 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 6bd8d9688f9..ca776267aa8 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 @@ byte[] provideKeyResponse(byte[] scope, byte[] response) * @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 609abd4e1e9..e77504c91cb 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 FrameworkMediaCrypto createMediaCrypto(byte[] initData) throws MediaCrypt adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents); } + @Override + public Class 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 c864adfa68f..d9d81cf6d43 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 @@ protected int supportsFormat(MediaCodecSelector mediaCodecSelector, 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 96bb606eae9..fe2a8c7d4b1 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 void testParcelable() { /* 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 8948173f606..ebadb0a08af 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.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 CastOptions getCastOptions(Context context) { @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 2d74bce5f16..9b15aff846d 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 @@ protected FlacDecoderException createUnexpectedDecodeException(Throwable error) } @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 de038921aa5..a97d99fa541 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 @@ public FlacFrameDecodeException(String message, int errorCode) { } } - 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 @@ public FlacDecoderJni() throws FlacDecoderException { /** * 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 @@ public void setData(ByteBuffer byteBufferData) { /** * 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 @@ public boolean isEndOfData() { /** * 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 @@ public int read(ByteBuffer target) throws IOException, InterruptedException { 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 @@ public void release() { 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 56acbdb7d36..0795079c6b1 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 f8ec477b880..dbce33b923c 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 @@ protected OpusDecoderException createUnexpectedDecodeException(Throwable error) } @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 cd5d67f686f..5529701c060 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 static boolean isAvailable() { 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 57e5481b557..0e13e826300 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 @@ protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { } @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 2c25f674d66..5106ab67ada 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 static boolean isAvailable() { 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 static String getVersion() { * 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 f8204f6be32..b5650860e9a 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 @@ private void releaseOutputBufferInternal(O outputBuffer) { * @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 ac7646cc4ba..376d0fd75e9 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 LibflacAudioRenderer() { * @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 c58293dd456..fe74f5c59c4 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 LibopusAudioRenderer() { * @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 LibopusAudioRenderer( * 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 @@ protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) 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 5529701c060..d09d69bf03c 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 static String getVersion() { * 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 5106ab67ada..e620332fc89 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 static boolean isHighBitDepthSupported() { * 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 a97d99fa541..103c2176a06 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 @@ public FlacFrameDecodeException(String message, int errorCode) { @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 bdf55fe40db..c72bed1b5bc 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.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 ConcatenatingMediaSource( this.pendingOnCompletionActions = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; - window = new Timeline.Window(); - period = new Timeline.Period(); addMediaSources(Arrays.asList(mediaSources)); } @@ -435,33 +430,21 @@ public synchronized void prepareSourceInternal(@Nullable TransferListener mediaT } } - @Override - @SuppressWarnings("MissingSuperCall") - public void maybeThrowSourceInfoRefreshError() throws IOException { - // Do nothing. Source info refresh errors of the individual sources will be thrown when calling - // DeferredMediaPeriod.maybeThrowPrepareError. - } - @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); + MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); if (holder == null) { // Stale event. The media source has already been removed. - holder = new MediaSourceHolder(new DummyMediaSource()); - holder.hasStartedPreparing = true; - } - MaskingMediaPeriod mediaPeriod = - new MaskingMediaPeriod(holder.mediaSource, id, allocator, startPositionUs); - mediaSourceByMediaPeriod.put(mediaPeriod, holder); - holder.activeMediaPeriods.add(mediaPeriod); - if (!holder.hasStartedPreparing) { - holder.hasStartedPreparing = true; + holder = new MediaSourceHolder(new DummyMediaSource(), useLazyPreparation); + holder.isRemoved = true; prepareChildSource(holder, holder.mediaSource); - } else if (holder.isPrepared) { - MediaPeriodId idInSource = id.copyWithPeriodUid(getChildPeriodUid(holder, id.periodUid)); - mediaPeriod.createPeriod(idInSource); } + holder.activeMediaPeriodIds.add(childMediaPeriodId); + MediaPeriod mediaPeriod = + holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + mediaSourceByMediaPeriod.put(mediaPeriod, holder); return mediaPeriod; } @@ -469,8 +452,8 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star public void releasePeriod(MediaPeriod mediaPeriod) { MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); - ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); - holder.activeMediaPeriods.remove(mediaPeriod); + holder.mediaSource.releasePeriod(mediaPeriod); + holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); maybeReleaseChildSource(holder); } @@ -502,10 +485,10 @@ protected void onChildSourceInfoRefreshed( @Nullable protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) { - for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) { + 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.activeMediaPeriods.get(i).id.windowSequenceNumber + if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber == mediaPeriodId.windowSequenceNumber) { Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); return mediaPeriodId.copyWithPeriodUid(periodUid); @@ -535,7 +518,7 @@ private void addPublicMediaSources( } List 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 @@ private void addMediaSourcesInternal( private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) { if (newIndex > 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 @@ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Time mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate); } } - if (mediaSourceHolder.isPrepared) { - mediaSourceHolder.timeline = deferredTimeline.cloneWithUpdatedTimeline(timeline); - } else if (timeline.isEmpty()) { - mediaSourceHolder.timeline = - DeferredTimeline.createWithRealTimeline(timeline, DeferredTimeline.DUMMY_ID); - } else { - // We should have at most one deferred media period for the DummyTimeline 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. - Assertions.checkState(mediaSourceHolder.activeMediaPeriods.size() <= 1); - MaskingMediaPeriod deferredMediaPeriod = - mediaSourceHolder.activeMediaPeriods.isEmpty() - ? null - : mediaSourceHolder.activeMediaPeriods.get(0); - // 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. - timeline.getWindow(/* windowIndex= */ 0, window); - long windowStartPositionUs = window.getDefaultPositionUs(); - if (deferredMediaPeriod != null) { - long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs(); - if (periodPreparePositionUs != 0) { - windowStartPositionUs = periodPreparePositionUs; - } - } - Pair 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 @@ private void moveMediaSourceInternal(int currentIndex, int newIndex) { MediaSourceHolder holder = mediaSourceHolders.get(i); holder.childIndex = i; holder.firstWindowIndexInChild = windowOffset; - windowOffset += holder.timeline.getWindowCount(); + windowOffset += holder.mediaSource.getTimeline().getWindowCount(); } } @@ -846,11 +774,8 @@ private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffs } private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { - // Release if the source has been removed from the playlist, but only if it has been previously - // prepared and only if we are not waiting for an existing media period to be released. - if (mediaSourceHolder.isRemoved - && mediaSourceHolder.hasStartedPreparing - && mediaSourceHolder.activeMediaPeriods.isEmpty()) { + // Release if the source has been removed from the playlist and no periods are still active. + if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { releaseChildSource(mediaSourceHolder); } } @@ -861,46 +786,36 @@ private static Object getMediaSourceHolderUid(Object periodUid) { } /** Return uid of child period from period uid of concatenated source. */ - private static Object getChildPeriodUid(MediaSourceHolder holder, Object periodUid) { - Object childUid = ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); - return childUid.equals(DeferredTimeline.DUMMY_ID) ? holder.timeline.replacedId : childUid; + private static Object getChildPeriodUid(Object periodUid) { + return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); } private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { - if (holder.timeline.replacedId.equals(childPeriodUid)) { - childPeriodUid = DeferredTimeline.DUMMY_ID; - } return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid); } /** Data class to hold playlist media sources together with meta data needed to process them. */ /* package */ static final class MediaSourceHolder { - public final MediaSource mediaSource; + public final MaskingMediaSource mediaSource; public final Object uid; - public final List activeMediaPeriods; + public final List activeMediaPeriodIds; - public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - public 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 ConcatenatedTimeline( int windowCount = 0; int periodCount = 0; for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - timelines[index] = mediaSourceHolder.timeline; + timelines[index] = mediaSourceHolder.mediaSource.getTimeline(); firstWindowInChildIndices[index] = windowCount; firstPeriodInChildIndices[index] = periodCount; windowCount += timelines[index].getWindowCount(); @@ -1003,135 +918,6 @@ public int getPeriodCount() { } } - /** - * Timeline used as placeholder for an unprepared media source. After preparation, a - * DeferredTimeline is used to keep the originally assigned dummy period ID. - */ - private static final class DeferredTimeline extends ForwardingTimeline { - - private static final Object DUMMY_ID = new Object(); - - private final Object replacedId; - - /** - * Returns an instance with a dummy timeline using the provided window tag. - * - * @param windowTag A window tag. - */ - public static DeferredTimeline createWithDummyTimeline(@Nullable Object windowTag) { - return new DeferredTimeline(new DummyTimeline(windowTag), DUMMY_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 DeferredTimeline createWithRealTimeline( - Timeline timeline, Object firstPeriodUid) { - return new DeferredTimeline(timeline, firstPeriodUid); - } - - private DeferredTimeline(Timeline timeline, Object replacedId) { - super(timeline); - this.replacedId = replacedId; - } - - /** - * Returns a copy with an updated timeline. This keeps the existing period replacement. - * - * @param timeline The new timeline. - */ - public DeferredTimeline cloneWithUpdatedTimeline(Timeline timeline) { - return new DeferredTimeline(timeline, replacedId); - } - - /** Returns 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, replacedId)) { - period.uid = DUMMY_ID; - } - return period; - } - - @Override - public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid); - } - - @Override - public Object getUidOfPeriod(int periodIndex) { - Object uid = timeline.getUidOfPeriod(periodIndex); - return Util.areEqual(uid, replacedId) ? DUMMY_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= */ DeferredTimeline.DUMMY_ID, - /* windowIndex= */ 0, - /* durationUs = */ C.TIME_UNSET, - /* positionInWindowUs= */ 0); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return uid == DeferredTimeline.DUMMY_ID ? 0 : C.INDEX_UNSET; - } - - @Override - public Object getUidOfPeriod(int periodIndex) { - return DeferredTimeline.DUMMY_ID; - } - } - /** Dummy media source which does nothing and does not support creating periods. */ private static final class DummyMediaSource extends BaseMediaSource { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java new file mode 100644 index 00000000000..ad9ef194da1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import androidx.annotation.Nullable; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media + * structure is known. + */ +public final class MaskingMediaSource extends CompositeMediaSource { + + private final MediaSource mediaSource; + private final boolean useLazyPreparation; + private final Timeline.Window window; + private final Timeline.Period period; + + private MaskingTimeline timeline; + @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + 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 8137289555b..5187addec39 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 void testPlaylistWithLazyMediaSource() throws IOException, InterruptedExc 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 | 35 ++-- .../exoplayer2/ext/flac/FlacExtractor.java | 182 ++++++++---------- 5 files changed, 114 insertions(+), 117 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 435279fc452..934d7cf1063 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 void testGetSeekMap_returnsSeekMapWithCorrectDuration() 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 void testSetSeekTargetUs_returnsSeekPending() 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 d9cbac6ad5c..97f152cea47 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 @@ 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 9b15aff846d..d20c18e9571 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 @@ 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 @@ public FlacDecoder( 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 103c2176a06..32ef22dab06 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 @@ 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 @@ public FlacFrameDecodeException(String message, int errorCode) { @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 @@ public FlacDecoderJni() throws FlacDecoderException { } /** - * 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; + endOfExtractorInput = false; if (tempBuffer == null) { - this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; + tempBuffer = new byte[TEMP_BUFFER_SIZE]; } - endOfExtractorInput = false; } + /** + * 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 @@ public boolean isEndOfData() { *

        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 @@ public int read(ByteBuffer target) throws IOException, InterruptedException { } /** 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 79350e6ae3c..491b9621296 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.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 FlacExtractor() { */ 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 boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept @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); + try { + decodeStreamInfo(input); - if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); - } + if (binarySearchSeeker != null && binarySearchSeeker.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; - } + 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; + } - writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); - return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + 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 void release() { } /** - * 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 @@ private Metadata peekId3Data(ExtractorInput input) throws IOException, Interrupt */ 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); - extractorOutput.seekMap(seekMap); + 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 SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET) { + 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(); - flacBinarySearchSeeker = + binarySearchSeeker = 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()); + seekMap = binarySearchSeeker.getSeekMap(); + } else { + seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); } + extractorOutput.seekMap(seekMap); } - private void outputFormat(FlacStreamInfo streamInfo) { + private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, @@ -277,25 +271,15 @@ private void outputFormat(FlacStreamInfo streamInfo) { /* 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 491b9621296..b50554e2f6c 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.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 int read(final ExtractorInput input, PositionHolder seekPosition) 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 @@ private Metadata peekId3Data(ExtractorInput input) throws IOException, Interrupt 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 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, 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 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, 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 @@ private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) { /* 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 06a58884046..10b244cb39a 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 b50554e2f6c..082068f34df 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.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 FlacExtractor() { * @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 int read(final ExtractorInput input, PositionHolder seekPosition) 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 @@ private Metadata peekId3Data(ExtractorInput input) throws IOException, Interrupt 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 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, 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 @@ private static FlacBinarySearchSeeker outputSeekMap( } 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 d76ca54b7b3..855b0e94f23 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 f982efa9a7d..e3f753958e7 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 @@ private void flushAudioProcessors() { } } - 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 @@ private void initialize() throws InitializationException { } } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + applyPlaybackParameters(playbackParameters, presentationTimeUs); audioTrackPositionTracker.setAudioTrack( audioTrack, @@ -591,15 +587,12 @@ public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) 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 boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) } 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 PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParam // 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 void run() { }.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 3c34d9b577c..7c2c8a6e0b1 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 void setAdWillPlayMuted(boolean b) { 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 eb1cc8f336d..806cc9fad44 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 @@ private long durationUs; public ScriptTagPayloadReader() { - super(null); + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -138,7 +140,10 @@ private static ArrayList readAmfStrictArray(ParsableByteArray data) { 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 @@ private static HashMap readAmfObject(ParsableByteArray data) { 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 @@ private static HashMap readAmfEcmaArray(ParsableByteArray data) 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 @@ private static Date readAmfDate(ParsableByteArray data) { return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -208,8 +220,8 @@ private static Object readAmfData(ParsableByteArray data, int type) { 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 9d3635e8b39..7676926c4dd 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 Track(int id, int type, long timescale, long movieTimescale, long duratio * @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 5bd29c6e756..a35d211aa46 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 29facdb2106..39359a9367f 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 @@ */ 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 38d6ff25cb7..bd561afaf8b 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 @@ protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) { @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 b34628b9227..1dcdecf95f0 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 @@ 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 void setContent(long timeUs, Subtitle subtitle, long subsampleOffsetUs) { @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 091bda49f32..9ef3556c8fa 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 @@ private void maybeInflateData(ParsableByteArray buffer) { } } + @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); @@ -197,6 +199,7 @@ private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) 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 d701f99d734..e305259cbc7 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 SsaDecoder() { * 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 @@ private void parseDialogueLine(String dialogueLine, List cues, LongArray cu 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 339119ed6b5..9a3756194f0 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 @@ 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 @@ public long getEventTime(int index) { @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 cf174283ec7..eb2b704bee9 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { // 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { 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 a79df478e57..01ed1711a9e 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 @@ 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 @@ public long getEventTime(int index) { @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 89017a40c05..c8f2979c58e 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) } 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 a3e602e404f..23b2300dfab 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 void clearAllKeyRequestProperties() { 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 byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception 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 @@ private static byte[] executePost( 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 | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 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 774b94a43cc..9d4b914d76c 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 @@ protected void decode(SubtitleInputBuffer inputBuffer) { continue; } - // Special North American character set. - // ccData1 - 0|0|0|1|C|0|0|1 - // ccData2 - 0|0|1|1|X|X|X|X - if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { - if (getChannel(ccData1) == selectedChannel) { - currentCueBuilder.append(getSpecialChar(ccData2)); - } + if (!updateAndVerifyCurrentChannel(ccData1)) { + // Wrong channel. continue; } - // Extended Western European character set. - // ccData1 - 0|0|0|1|C|0|1|S - // ccData2 - 0|0|1|X|X|X|X|X - if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { - if (getChannel(ccData1) == selectedChannel) { - // Remove standard equivalent of the special extended char before appending new one + if (isCtrlCode(ccData1)) { + if (isSpecialChar(ccData1, ccData2)) { + // Special North American character. + currentCueBuilder.append(getSpecialChar(ccData2)); + } 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 @@ protected void decode(SubtitleInputBuffer inputBuffer) { } } + 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 @@ private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { } } - if (currentChannel != selectedChannel) { - return; - } - if (isMidrowCtrlCode(cc1, cc2)) { handleMidrowCtrl(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { @@ -681,11 +665,33 @@ private static char getChar(byte ccData) { 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 @@ private static char getExtendedPtDeChar(byte ccData) { 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 db6f71286ee..03518ac18a2 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 CastPlayer(CastContext castContext) { * 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 PendingResult loadItem(MediaQueueItem item, long posi * @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 PendingResult loadItems(MediaQueueItem[] items, int s * @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 PendingResult addItems(MediaQueueItem... items) { * @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 PendingResult addItems(int periodId, MediaQueueItem.. * @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 PendingResult removeItem(int periodId) { * @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 PendingResult moveItem(int periodId, int newIndex) { * @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 boolean isCastSessionAvailable() { /** * 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 int getPlaybackState() { } @Override + @Nullable public ExoPlaybackException getPlaybackError() { return null; } @@ -530,7 +537,7 @@ public long getContentBufferedPosition() { // 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 @@ private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) } } - 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 d4a37ea4ef6..268219b6d5d 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 @@ 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 @@ void onAspectRatioUpdated( 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 void setAspectRatio(float widthHeightRatio) { /** * 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 b9b24567225..bba422e4880 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 interface ProgressUpdateListener { 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 5d99eda1096..0bdc1acc885 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 void setBottomPaddingFraction(float bottomPaddingFraction) { @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 void dispatchDraw(Canvas canvas) { 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 855b0e94f23..fd748d45ab4 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 9d4b914d76c..5a14063aa1b 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 void flush() { setCaptionMode(CC_MODE_UNKNOWN); setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionValid = false; + isCaptionValid = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -358,13 +358,19 @@ protected void decode(SubtitleInputBuffer inputBuffer) { 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]; + + if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) { + // Ignore repeated valid commands. + continue; + } - boolean previousCaptionValid = captionValid; - captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG; - if (!captionValid) { - if (previousCaptionValid) { + 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 @@ protected void decode(SubtitleInputBuffer inputBuffer) { 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 @@ protected void decode(SubtitleInputBuffer inputBuffer) { } 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 @@ private boolean updateAndVerifyCurrentChannel(byte cc1) { 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 @@ private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { 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 @@ private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) { } } - 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 3fe22a27626..d9770415f95 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 e7cf87ed6ec..4858eec6b78 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 @@ private static final class MediaPreparer 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 MediaPreparer(MediaSource mediaSource, DownloadHelper 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 MediaPreparer(MediaSource mediaSource, DownloadHelper 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 fd748d45ab4..5a1afdd559e 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 d9770415f95..e857d5a812d 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 db3f3943e10..190f4de5a6c 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 32ef22dab06..448e2a1b05e 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.Util; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -151,6 +152,12 @@ public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedExceptio 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 @@ private int readFromExtractorInput( 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 082068f34df..307cdfa8c8a 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.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.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 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, } 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 @@ private static boolean peekFlacSignature(ExtractorInput input) 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 298719d48d0..0971ba5883a 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 83d3367415c..06c98302fda 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 cea7fbe33be..aec07d673e9 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 00000000000..a2125326851 --- /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 00000000000..2deb5b11277 --- /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 00000000000..e2c2bcf021e --- /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 00000000000..218de9649d6 --- /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 96ec3d2f103..f293673c498 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 5a1afdd559e..e40bfe8d818 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 448e2a1b05e..32ef22dab06 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.Util; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -152,12 +151,6 @@ public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedExceptio 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 @@ private int readFromExtractorInput( 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 307cdfa8c8a..50e0458fd7e 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.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.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 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, } 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 @@ private static boolean peekFlacSignature(ExtractorInput input) 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 0971ba5883a..600f181890d 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 06c98302fda..9af7ec5c8a2 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 aec07d673e9..8b70fea4cba 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 a2125326851..00000000000 --- 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 2deb5b11277..16bf3339026 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 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 0df39e103df..2d70402bdbb 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 FlacStreamInfo(byte[] data, int offset) { 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 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 e2c2bcf021e..11b373327b4 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 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 ee0a9fa5b53..b44dab34455 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 934d7cf1063..b469a92cb44 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 void testGetSeekMap_returnsSeekMapWithCorrectDuration() 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 void testSetSeekTargetUs_returnsSeekPending() 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 b9c6ea06dd4..4bfcc003ec9 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.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 @@ 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 @@ public TimestampSearchResult searchForTimestamp( * 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 d20c18e9571..50eb048d98b 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.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 @@ public FlacDecoder( } 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 @@ public FlacDecoder( } 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 32ef22dab06..bf9dff9e52c 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 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 @@ public int read(ByteBuffer target) throws IOException, InterruptedException { 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 @@ private int readFromExtractorInput( 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 50e0458fd7e..5061cb614f4 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.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 @@ private FlacDecoderJni initDecoderJni(ExtractorInput input) { } @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 @@ private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, } 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 @@ private static boolean peekFlacSignature(ExtractorInput input) @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 600f181890d..22b581489f8 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 5eb0727908d..d4c2bbb485d 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.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 @@ 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 @@ public static boolean verifyBitstreamType(ParsableByteArray data) { protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -71,14 +71,24 @@ protected long preparePayload(ParsableByteArray packet) { 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 @@ public SeekPoints getSeekPoints(long timeUs) { @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 2d70402bdbb..abccbd3c030 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 @@ /** * 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 FlacStreamInfo(byte[] data, int offset) { } /** - * 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 FlacStreamInfo(byte[] data, int offset) { * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, @@ -98,7 +98,7 @@ public 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 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 11b373327b4..504dce62b95 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 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 void decode() { 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 void decode() { 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 void decodeTwoSeparators() { 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 void decodeNoSeparators() { 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 949bd178eaa..511a974a0eb 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 void experimental_allowMultipleAdaptiveSelections() { } } - 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 @@ private static boolean isSupportedAdaptiveAudioTrack( * @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 @@ protected Pair selectTextTrack( 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 int hashCode() { } + /** 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 e40bfe8d818..59f30b8a0ad 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 c92ebd7e94c..d2a1ca08608 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 @@ public void onPositionDiscontinuity(@DiscontinuityReason int reason) { } @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 03518ac18a2..6a33aa04281 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 Timeline getCurrentTimeline() { return currentTimeline; } - @Override - @Nullable public Object getCurrentManifest() { - return null; - } - @Override public int getCurrentPeriodIndex() { return getCurrentWindowIndex(); @@ -592,8 +587,7 @@ private void maybeUpdateTimelineAndNotify() { 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 800c19047b2..b84f1c1f2b4 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 @@ public Window getWindow( 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 5a266c290dd..249271dc612 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 void resumeAd() { // 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 a9d6a37fac8..a9572b7a8d9 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 @@ public FakePlayer() { 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 1fece6bc8e9..370e5515e82 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 void onPlayerError(ExoPlaybackException exception) { } @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 3136e3cca98..be085ae30ba 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 @@ private class ComponentListener extends MediaSessionCompat.Callback // 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 774f1b452c7..bb14ac147b4 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 final int getPreviousWindowIndex() { @Override @Nullable public final Object getCurrentTag() { - int windowIndex = getCurrentWindowIndex(); Timeline timeline = getCurrentTimeline(); - return windowIndex >= timeline.getWindowCount() + return timeline.isEmpty() + ? null + : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ true).tag; + } + + @Override + @Nullable + public final Object getCurrentManifest() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() ? null - : timeline.getWindow(windowIndex, window, /* setTag= */ true).tag; + : 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 945bd32d303..73107aa98e6 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 @@ public Timeline getCurrentTimeline() { 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 @@ private PlaybackInfo getResetPlaybackInfo( 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 @@ private static final class PlaybackInfoUpdate implements Runnable { 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 @@ public PlaybackInfoUpdate( 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 @@ public PlaybackInfoUpdate( @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 a6d43528803..5f53427fca4 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 @@ public Looper getPlaybackLooper() { // 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 @@ private void resetInternal( playbackInfo = new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, @@ -1276,9 +1276,8 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) 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 @@ private static final class MediaSourceRefreshInfo { 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 d3e4a0e6269..1eedae08b6b 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 @@ /** 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 @@ public static PlaybackInfo createDummy( long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) { return new PlaybackInfo( Timeline.EMPTY, - /* manifest= */ null, DUMMY_MEDIA_PERIOD_ID, startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, @@ -109,7 +105,6 @@ public static PlaybackInfo createDummy( * 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 @@ public static PlaybackInfo createDummy( */ public PlaybackInfo( Timeline timeline, - @Nullable Object manifest, MediaPeriodId periodId, long startPositionUs, long contentPositionUs, @@ -137,7 +131,6 @@ public PlaybackInfo( long totalBufferedDurationUs, long positionUs) { this.timeline = timeline; - this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; @@ -187,7 +180,6 @@ public PlaybackInfo copyWithNewPosition( long totalBufferedDurationUs) { return new PlaybackInfo( timeline, - manifest, periodId, positionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET, @@ -202,17 +194,15 @@ public PlaybackInfo copyWithNewPosition( } /** - * 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 @@ public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { public PlaybackInfo copyWithPlaybackState(int playbackState) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -260,7 +249,6 @@ public PlaybackInfo copyWithPlaybackState(int playbackState) { public PlaybackInfo copyWithIsLoading(boolean isLoading) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -286,7 +274,6 @@ public PlaybackInfo copyWithTrackInfo( TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -310,7 +297,6 @@ public PlaybackInfo copyWithTrackInfo( 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 0e19212afab..68a386d2dee 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 @@ interface MetadataComponent { */ 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 @@ interface EventListener { * @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 @@ default void onPlayerError(ExoPlaybackException error) {} * 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 @@ default void onSeekProcessed() {} @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 void onTimelineChanged( 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 b427991d6ee..a782255cb87 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 Timeline getCurrentTimeline() { 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 0c64810d589..32fa3a6e4bc 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 static final class Window { /** 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 static final class Window { /** 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 Window set( 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 deecfb15a8e..de0f1773422 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 final void onDownstreamFormatChanged( // 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 4858eec6b78..17bc304db3f 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 static MediaSource createMediaSource( 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 DownloadHelper( trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); callbackHandler = new Handler(Util.getLooper()); + window = new Timeline.Window(); } /** @@ -409,7 +411,9 @@ public Object getManifest() { return null; } assertPreparedWithMedia(); - return mediaPreparer.manifest; + return mediaPreparer.timeline.getWindowCount() > 0 + ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest + : null; } /** @@ -814,7 +818,6 @@ private static final class MediaPreparer 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 boolean handleMessage(Message msg) { // 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 f6ea3da0890..124f70c64c9 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 @@ * 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 BaseMediaSource() { * 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 final void prepareSource( 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 final void releaseSource(SourceInfoRefreshListener listener) { 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 c942f9320e7..81169354de0 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 @@ private static String getReasonDescription(@Reason int reason) { 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 @@ protected void releaseSourceInternal() { } @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 @@ private void refreshClippedTimeline(Timeline timeline) { 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 1a9e1ff250c..612ad33f9d7 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 @@ protected void releaseSourceInternal() { * @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 @@ protected abstract void onChildSourceInfoRefreshed( 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 c72bed1b5bc..18d5c49fb45 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 synchronized void releaseSourceInternal() { @Override protected void onChildSourceInfoRefreshed( - MediaSourceHolder mediaSourceHolder, - MediaSource mediaSource, - Timeline timeline, - @Nullable Object manifest) { + MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) { updateMediaSourceInternal(mediaSourceHolder, timeline); } @@ -679,8 +676,7 @@ private void updateTimelineAndScheduleOnCompletionActions() { timelineUpdateScheduled = false; Set 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 f07ee63e790..2bcaad4fce6 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 @@ protected void releaseSourceInternal() { } @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 7adb18dc946..ac23e2a8317 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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 ad9ef194da1..1fca8249103 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 void releaseSourceInternal() { @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 @@ protected void onChildSourceInfoRefreshed( } } isPrepared = true; - refreshSourceInfo(this.timeline, manifest); + refreshSourceInfo(this.timeline); } @Nullable @@ -274,6 +274,7 @@ public Window getWindow( int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { return window.set( tag, + /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, /* isSeekable= */ false, 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 b40bbb35d12..f86be8afc2c 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 @@ interface Callback extends SequenceableLoader.Callback { * *

        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 82359ffccdf..10e29f3f445 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 f12ce92f54b..dd7675f3d48 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 IllegalMergeException(@Reason int reason) { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - @Nullable private Object primaryManifest; private int periodCount; @Nullable private IllegalMergeException mergeError; @@ -143,7 +142,6 @@ public void releasePeriod(MediaPeriod mediaPeriod) { protected void releaseSourceInternal() { super.releaseSourceInternal(); Arrays.fill(timelines, null); - primaryManifest = null; periodCount = PERIOD_COUNT_UNSET; mergeError = null; pendingTimelineSources.clear(); @@ -152,7 +150,7 @@ protected void releaseSourceInternal() { @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 @@ protected void onChildSourceInfoRefreshed( } 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 ba69b46d7f1..42ec237b3e5 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 @@ private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { // 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 fc99e8cb7bf..a5b78ef3f7f 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 SilenceMediaSource(long durationUs) { @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 14648775f8d..8790b09f075 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 SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynam * @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 SinglePeriodTimeline( /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + manifest, tag); } @@ -80,6 +87,7 @@ public SinglePeriodTimeline( * 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 SinglePeriodTimeline( long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + @Nullable Object manifest, @Nullable Object tag) { this( /* presentationStartTimeMs= */ C.TIME_UNSET, @@ -99,6 +108,7 @@ public SinglePeriodTimeline( windowDefaultStartPositionUs, isSeekable, isDynamic, + manifest, tag); } @@ -117,6 +127,7 @@ public SinglePeriodTimeline( * 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 SinglePeriodTimeline( long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; @@ -137,6 +149,7 @@ public SinglePeriodTimeline( this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.manifest = manifest; this.tag = tag; } @@ -165,6 +178,7 @@ public Window getWindow( } 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 6c1881a01a6..04ee3a153c4 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 @@ private SingleSampleMediaSource( 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 Object getTag() { @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 a6c2cf27673..5e22de43203 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 RuntimeException getRuntimeExceptionForUnexpected() { // 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 @@ protected void releaseSourceInternal() { componentListener = null; maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; - contentManifest = null; adPlaybackState = null; adGroupMediaSources = new MediaSource[0][]; adGroupTimelines = new Timeline[0][]; @@ -274,16 +272,13 @@ protected void releaseSourceInternal() { @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 @@ private void onAdPlaybackState(AdPlaybackState adPlaybackState) { 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 @@ private void maybeUpdateSourceInfo() { 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 639e80348b7..61b84184116 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 void testPlayEmptyTimeline() throws Exception { /** 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 void testPlaySinglePeriodTimeline() throws Exception { .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 boolean isEnded() { @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 synchronized void prepareSourceInternal( } }; 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 synchronized void prepareSourceInternal( // 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 synchronized void prepareSourceInternal( // 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 void testRepeatModeChanges() throws Exception { 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 void testAdGroupWithLoadErrorIsSkipped() throws Exception { /* 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 void testSeekDiscontinuity() throws Exception { 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 @@ protected FakeMediaPeriod createFakeMediaPeriod( 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 @@ protected FakeMediaPeriod createFakeMediaPeriod( 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 @@ protected FakeMediaPeriod createFakeMediaPeriod( 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 void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Ex 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 void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemad 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 void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReuse 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 void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReuse 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 void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() thro 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 void testSetPlaybackParametersBeforePreparationCompletesSucceeds() throws 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 void run(SimpleExoPlayer player) { @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 void testStopWithoutResetReleasesMediaSource() throws Exception { @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 void testStopWithResetReleasesMediaSource() throws Exception { @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 void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { 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 void testReprepareAndKeepPositionWithNewMediaSource() throws Exception { 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 void testReprepareAfterPlaybackError() throws Exception { .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 void run(SimpleExoPlayer player) { } }) .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 void run(SimpleExoPlayer player) { @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 void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() 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 void run(SimpleExoPlayer player) { 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 void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception 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 void run(SimpleExoPlayer player) { @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 void testSendMessagesAtStartAndEndOfPeriod() throws Exception { // 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 void testSendMessagesMoveCurrentWindowIndex() throws Exception { 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 void testSendMessagesMoveWindowIndex() throws Exception { 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 void testSendMessagesMoveWindowIndex() throws Exception { 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 void testTimelineUpdateDropsPrebufferedPeriods() throws Exception { 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 void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNu /* 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 void testRecursivePlayerChangesAreReportedInCorrectOrder() throws Excepti 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 void testClippedLoopedPeriodsArePlayedFully() throws Exception { 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 void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackG 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 void testSecondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRe /* 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 void maybeThrowSourceInfoRefreshError() throws IOException { /* 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 void failingDynamicUpdateOnlyThrowsWhenAvailablePeriodHasBeenFullyRead() /* 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 void removingLoopingLastPeriodFromPlaylistDoesNotThrow() throws Exception 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 void removingLoopingLastPeriodFromPlaylistDoesNotThrow() throws Exception 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 void seekToUnpreparedWindowWithMultiplePeriodsInConcatenationStartsAtCorr /* 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 73f42c5fc97..def7f8552e9 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 @@ private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { 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 22aa63b83ad..a2546adfe4e 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 void testSinglePeriod() throws Exception { FakeMediaSource mediaSource = new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); @@ -183,12 +184,10 @@ public void testAutomaticPeriodTransition() throws Exception { 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 void testAutomaticPeriodTransition() throws Exception { 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 void testPeriodTransitionWithRendererChange() throws Exception { 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 void testSeekToOtherPeriod() throws Exception { 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 void testSeekBackAfterReadingAhead() throws Exception { @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 void testPrepareNewSource() throws Exception { @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 void testReprepareAfterError() throws Exception { @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 void testDynamicTimelineChange() throws Exception { @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 3b78a2e3ae3..479936b82f6 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 @@ private static void assertTrackSelectionEquals( 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 846600f243e..89acb3ec3e5 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 void testClippingFromDefaultPosition() throws IOException { /* 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 void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOExceptio /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -215,6 +217,7 @@ public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOExceptio /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -253,6 +256,7 @@ public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -262,6 +266,7 @@ public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -300,6 +305,7 @@ public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -309,6 +315,7 @@ public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -348,6 +355,7 @@ public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOEx /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -357,6 +365,7 @@ public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOEx /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -473,7 +482,7 @@ private static MediaLoadData getClippingMediaSourceMediaLoadData( 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 void onDownstreamFormatChanged( */ 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 @@ private static Timeline getClippedTimeline(Timeline timeline, long startUs, long */ 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 @@ private static Timeline[] getClippedTimelines( 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 5187addec39..c587d85a853 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 void testPlaylistWithLazyMediaSource() throws IOException, InterruptedExc 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 void testEmptyTimelineMediaSource() throws IOException, InterruptedExcept 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 void testEmptyTimelineMediaSource() throws IOException, InterruptedExcept 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 void testDynamicChangeOfEmptyTimelines() throws IOException { @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 void testPeriodCreationWithAds() throws IOException, InterruptedException 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 void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { @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 void testDuplicateMediaSources() throws IOException, InterruptedException @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 void testDuplicateNestedMediaSources() throws IOException, InterruptedExc 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 @@ private void assertCompletedAllMediaPeriodLoads(Timeline timeline) { 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 df6506ed528..fa7c2f06145 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 void testInfiniteLoopPeriodCreation() throws Exception { * 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 @@ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) thr * 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 5ea15ac2e8a..1434d285001 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 void testMergingTimelinesWithDifferentPeriodCounts() throws IOException { 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 void testMergingMediaSourcePeriodCreation() throws Exception { 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 bdd6820efa8..701ec3521c5 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 void testGetPeriodPositionDynamicWindowKnownDuration() { /* 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 void setNullTag_returnsNullTag_butUsesDefaultUid() { /* 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 void setTag_isUsedForWindowTag() { 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 void getIndexOfPeriod_returnsPeriod() { /* 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 551555502fd..b9cb9010414 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 @@ private void processManifest(boolean scheduleRefresh) { windowDefaultStartPositionUs, manifest, tag); - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!sideloadedManifest) { // Remove any pending simulated refresh. @@ -1193,6 +1193,7 @@ public Window getWindow( && 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 f891670e780..12c6a8ee728 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 void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { ? 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 void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + manifest, tag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { @@ -418,9 +420,10 @@ public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { 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 e31fbccae50..c053f255fca 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 @@ private void processManifest() { /* 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 @@ private void processManifest() { defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + manifest, tag); } else { long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs @@ -702,9 +704,10 @@ private void processManifest() { /* 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 bba422e4880..e408035e982 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 void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { } @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 c800c7bd637..9ad951cb171 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 void onPlayerStateChanged(boolean playWhenReady, @Player.State int playba } @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 93e52bc23a5..5d07f986d21 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 @@ protected void doActionImpl( } } - /** - * 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 @@ protected void doActionAndScheduleNextImpl( 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 cc4b3a60d73..f7c6694409a 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 ExoPlayerTestRunner build(Context context) { } 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 ExoPlayerTestRunner build(Context context) { 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 @@ private ExoPlayerTestRunner( 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 ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) // 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 void assertTimelinesEqual(Timeline... timelines) { 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 @@ private void handleException(Exception exception) { // 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 5a158a36598..0d97b7a20ff 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 80456169ffd..8e5ba230ac2 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 FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... f * 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 synchronized void setNewSourceInfo(final Timeline newTimeline, final Obje assertThat(releasedSource).isFalse(); assertThat(preparedSource).isTrue(); timeline = newTimeline; - manifest = newManifest; finishSourcePreparation(); }); } else { timeline = newTimeline; - manifest = newManifest; } } @@ -212,7 +207,7 @@ protected FakeMediaPeriod createFakeMediaPeriod( } 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 56438a51efc..58ee32cdd9f 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 TimelineWindowDefinition( 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 static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... a * 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 FakeTimeline(int windowCount) { * @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 Window getWindow( 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 0873dbd1455..6d626088fc2 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 @@ private class MediaSourceListener // 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 df96b634ddc..eaebe5a12de 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 TrackSelectionArray getCurrentTrackSelections() { 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 4f0c5b87aa5..42453929c4b 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.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 DecryptableSampleQueueReader(SampleQueue upstream, DrmSessionManager s 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 @@ private void onFormat(Format format) { 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 b1f907cc373..ed925230171 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 int read(byte[] buffer, int offset, int readLength) throws 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 42453929c4b..e33afaee604 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 int read( 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 int read( 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 int read( * 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 @@ private void onFormat(Format format) { } else { currentSession = null; } + outputFormatHolder.drmSession = currentSession; if (previousSession != null) { previousSession.releaseReference(); @@ -211,8 +219,9 @@ public boolean isReady(boolean loadingFinished) { } 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 b4db4971cc2..56f5fd2d099 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 @@ private void setDecoderDrmSession(@Nullable DrmSession session) /** * Called when a new format is read from the upstream source. * - * @param newFormat The new format. + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. */ @CallSuper @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 @@ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackExceptio 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 @@ private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackExcepti 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 93c60f29170..ef0207517ac 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 @@ private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackExcep return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { @@ -656,9 +656,9 @@ private void setDecoderDrmSession(@Nullable DrmSession session) } @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 @@ private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException 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 @@ private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException 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 929b579b4c6..59d861e13df 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.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 @@ private void initializePlayer() { 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 @@ private void initializePlayer() { finish(); return; } + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); } TrackSelection.Factory trackSelectionFactory; @@ -425,7 +428,7 @@ private void initializePlayer() { 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 @@ private void initializePlayer() { } 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 @@ 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); + 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 e33afaee604..b0b10d4e987 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 int read( */ 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 @@ private void onFormat(Format format, FormatHolder outputFormatHolder) { } 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 efc3b475968..6eaeefec6b7 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.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 interface ReleaseCallback { 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 interface ReleaseCallback { * @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 ChunkSampleStream( Callback> callback, Allocator allocator, long positionUs, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; @@ -126,6 +133,8 @@ public ChunkSampleStream( 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 void release(@Nullable ReleaseCallback callback) { 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 void onLoaderReleased() { @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 int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 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 be0aa4f1545..b34b677d45f 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 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 @@ /* 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 @@ public DashMediaPeriod( int periodIndex, DashChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffsetMs, @@ -109,6 +113,7 @@ public DashMediaPeriod( 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 @@ public DashMediaPeriod( 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 @@ private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTr } 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 @@ private static Pair buildTrackGroups( int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos( + drmSessionManager, adaptationSets, groupedAdaptationSetIndices, primaryGroupCount, @@ -569,6 +577,7 @@ private static int identifyEmbeddedTracks( } private static int buildPrimaryAndEmbeddedTrackGroupInfos( + DrmSessionManager drmSessionManager, List adaptationSets, int[][] groupedAdaptationSetIndices, int primaryGroupCount, @@ -585,7 +594,14 @@ private static int buildPrimaryAndEmbeddedTrackGroupInfos( } 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 @@ private ChunkSampleStream buildSampleStream(TrackGroupInfo trac 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 b9cb9010414..92c47f2f120 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.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 static final class Factory implements MediaSourceFactory { 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 Factory( @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 Factory setTag(Object tag) { return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. See {@link * #setLoadErrorHandlingPolicy} for the default value. @@ -253,6 +271,7 @@ public DashMediaSource createMediaSource(DashManifest manifest) { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -313,6 +332,7 @@ public DashMediaSource createMediaSource(Uri manifestUri) { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -361,6 +381,7 @@ public int[] getSupportedTypes() { 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 DashMediaSource( /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* livePresentationDelayOverridesManifest= */ false, @@ -556,6 +578,7 @@ public DashMediaSource( 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 @@ private DashMediaSource( ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, boolean livePresentationDelayOverridesManifest, @@ -584,6 +608,7 @@ private DashMediaSource( 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 MediaPeriod createPeriod( 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 fa077df2097..f39a493e9f4 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.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 void getSteamKeys_isCompatibleWithDashManifestFilter() { 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 0b7c06f8138..73225f68c79 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 @@ 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 void staticDownloadHelperForDash_doesNotThrow() { 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 38781782eba..6d4db3fbbbd 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 @@ 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 @@ private ChunkSampleStream buildSampleStream(TrackSelection select 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 fea863c48ef..bcb97be2871 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 @@ protected SampleStream createSampleStream(TrackSelection trackSelection) { /* 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 df45323ca36..722ab946f0f 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 d834c097cf5..39b49da402a 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 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) { 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 @@ private void buildAndPrepareMainSampleStreamWrapper( 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 @@ private void buildAndPrepareAudioSampleStreamWrappers( 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 96704053cbe..079852c4d47 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.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 @@ public interface Callback extends SequenceableLoader.Callback optionalTrackGroups; // Indexed by track group. private int[] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; @@ -200,18 +202,20 @@ public void continuePreparing() { /** * 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 @@ public int getPrimaryTrackGroupIndex() { 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 @@ private void buildTracksFromSampleStreams() { } 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 59d861e13df..3ade9173c6e 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 @@ private MediaSource buildMediaSource( .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 6d4db3fbbbd..286ec82ed64 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 @@ 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 @@ 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 @@ public SsMediaPeriod( SsChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, @@ -70,11 +73,12 @@ public SsMediaPeriod( 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 @@ private ChunkSampleStream buildSampleStream(TrackSelection select 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 c053f255fca..3c0593200ee 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 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 static final class Factory implements MediaSourceFactory { @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 Factory( @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 Factory setTag(Object tag) { return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. See {@link * #setLoadErrorHandlingPolicy} for the default value. @@ -220,6 +238,7 @@ public SsMediaSource createMediaSource(SsManifest manifest) { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -279,6 +298,7 @@ public SsMediaSource createMediaSource(Uri manifestUri) { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -318,6 +338,7 @@ public int[] getSupportedTypes() { 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 SsMediaSource( /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* tag= */ null); @@ -483,6 +505,7 @@ public SsMediaSource( manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs, /* tag= */ null); @@ -498,6 +521,7 @@ private SsMediaSource( ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, @Nullable Object tag) { @@ -508,6 +532,7 @@ private SsMediaSource( 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 787659fffe9..b9c63f843db 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 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 void getSteamKeys_isCompatibleWithSsManifestFilter() { 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 f293673c498..8cfc2b2ea1a 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 68a386d2dee..4e062dcb5e7 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 void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { 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 10e29f3f445..bd87eb65099 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 @@ default Object getTag() { } /** - * 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 92c47f2f120..f7387ee77c0 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 int[] getSupportedTypes() { /** * 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 2bcaad4fce6..7332ed74e05 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 @@ /** @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 Object getTag() { @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 void releasePeriod(MediaPeriod mediaPeriod) { 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 3ade9173c6e..249223bff56 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 @@ private MediaSource buildMediaSource( 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 7332ed74e05..9e7da87766a 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 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 @@ private ExtractorMediaSource( 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 a56d14083e0..83145d04b0a 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.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 @@ interface Listener { 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 @@ interface Listener { @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 @@ public ProgressiveMediaPeriod( Uri uri, DataSource dataSource, Extractor[] extractors, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Listener listener, @@ -160,6 +164,7 @@ public ProgressiveMediaPeriod( int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.listener = listener; @@ -180,6 +185,7 @@ public ProgressiveMediaPeriod( 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 @@ public void release() { sampleQueue.discardToEnd(); } } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); callback = null; @@ -432,29 +441,32 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame // 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 @@ private TrackOutput prepareTrackOutput(TrackId id) { @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 @@ public boolean isReady() { @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 42ec237b3e5..bd32587bddc 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 @@ 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 static final class Factory implements MediaSourceFactory { 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 Factory(DataSource.Factory dataSourceFactory) { 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 Factory setTag(Object tag) { return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. @@ -172,6 +190,7 @@ public ProgressiveMediaSource createMediaSource(Uri uri) { uri, dataSourceFactory, extractorsFactory, + drmSessionManager, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, @@ -193,6 +212,7 @@ public int[] getSupportedTypes() { 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 int[] getSupportedTypes() { Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @@ -214,6 +235,7 @@ public int[] getSupportedTypes() { 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 2a5e6356687..12babc1688b 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 5f53427fca4..e313c9aedf6 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.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 @@ implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSource.SourceInfoRefreshListener, + MediaSourceCaller, PlaybackParameterListener, PlayerMessage.Sender { @@ -264,7 +265,7 @@ public Looper getPlaybackLooper() { 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 17bc304db3f..139c6ad7947 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.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 @@ private static MediaSource createMediaSourceInternal( } 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 boolean handleMessage(Message msg) { } } - // 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 124f70c64c9..886952f5c3b 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 @@ */ 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 BaseMediaSource() { */ 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 final void removeEventListener(MediaSourceEventListener eventListener) { @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 612ad33f9d7..3eac3df5fe0 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 void maybeThrowSourceInfoRefreshError() throws IOException { @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 @@ protected abstract void onChildSourceInfoRefreshed( */ 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 void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { */ 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 @@ protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) { 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 f86be8afc2c..f076eae32cc 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.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 @@ interface Callback extends SequenceableLoader.Callback { * {@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 bd87eb65099..c3219a03c1c 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 @@ *

          *
        • 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 @@ */ 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 @@ default Object getTag() { } /** - * 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 @@ void prepareSource( 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 c587d85a853..39f36a991b3 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.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 void testCustomCallbackIsCalledAfterRelease() throws IOException { 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 f7387ee77c0..576491b4645 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 int[] getSupportedTypes() { /** * 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 6d626088fc2..211e85d30c2 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.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 void releasePeriod(final MediaPeriod mediaPeriod) { 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 void release() { 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 3900dc8e938..84b6f5870da 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 int onStartCommand(Intent intent, int flags, int startId) { 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 void onTaskRemoved(Intent rootIntent) { @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 @@ private final class ForegroundNotificationUpdater { 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 ForegroundNotificationUpdater(int notificationId, long updateInterval) { this.notificationId = notificationId; this.updateInterval = updateInterval; this.handler = new Handler(Looper.getMainLooper()); - this.updateRunnable = this::update; } public void startPeriodicUpdates() { @@ -794,7 +794,7 @@ public void startPeriodicUpdates() { public void stopPeriodicUpdates() { periodicUpdatesStarted = false; - handler.removeCallbacks(updateRunnable); + handler.removeCallbacksAndMessages(null); } public void showNotificationIfNotAlready() { @@ -810,12 +810,12 @@ public void invalidate() { } 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 @@ private DownloadManagerHelper( 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 void onRequirementsStateChanged( } } 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 | 133 ++++++++++-------- 1 file changed, 76 insertions(+), 57 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 511a974a0eb..c0de66516bf 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 void experimental_allowMultipleAdaptiveSelections() { 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 @@ private static boolean isSupportedAdaptiveAudioTrack( 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 @@ protected Pair selectTextTrack( 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 int hashCode() { /** 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; + 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 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 TextTrackScore(Format format, Parameters parameters, int formatSupport) { */ @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 c0de66516bf..b5d282f8a7d 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 int hashCode() { /** 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 TextTrackScore( */ @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 12babc1688b..46515d8fa8c 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 e857d5a812d..c8136ea4711 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 e313c9aedf6..0fc7242279d 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 @@ private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boo 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 @@ private void resetInternal( 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 139c6ad7947..4b5bf3c8a4a 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 void release() { 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 542565e70d1..78b3a35549d 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 @@ public synchronized int peekNext(Format downstreamFormat) { 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 @@ public synchronized int read( 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 @@ public synchronized int read( } 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 @@ public synchronized boolean format(Format format) { } 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 d9043e95487..f09a22e951b 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 46515d8fa8c..9d66a6f800e 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 886952f5c3b..86e00e0a374 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.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 @@ 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 BaseMediaSource() { */ 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 @@ protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( 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 final void prepareSource( 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 3eac3df5fe0..3672c304ccb 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 void maybeThrowSourceInfoRefreshError() throws IOException { } } + @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 @@ protected final void prepareChildSource(final T id, MediaSource mediaSource) { 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 18d5c49fb45..669a0e7bb41 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.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 ConcatenatingMediaSource( this.mediaSourceHolders = new ArrayList<>(); 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 Object getTag() { } @Override - public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected synchronized void prepareSourceInternal( + @Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); if (mediaSourcesPublic.isEmpty()) { @@ -430,6 +434,12 @@ public synchronized void prepareSourceInternal(@Nullable TransferListener mediaT } } + @SuppressWarnings("MissingSuperCall") + @Override + protected void enableInternal() { + // Suppress enabling all child sources here as they can be lazily enabled when creating periods. + } + @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); @@ -441,10 +451,12 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star holder.isRemoved = true; prepareChildSource(holder, holder.mediaSource); } + enableMediaSource(holder); holder.activeMediaPeriodIds.add(childMediaPeriodId); MediaPeriod mediaPeriod = holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); mediaSourceByMediaPeriod.put(mediaPeriod, holder); + disableUnusedMediaSources(); return mediaPeriod; } @@ -454,13 +466,23 @@ public void releasePeriod(MediaPeriod mediaPeriod) { Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); holder.mediaSource.releasePeriod(mediaPeriod); holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); + if (!mediaSourceByMediaPeriod.isEmpty()) { + disableUnusedMediaSources(); + } maybeReleaseChildSource(holder); } @Override - public synchronized void releaseSourceInternal() { + protected void disableInternal() { + super.disableInternal(); + enabledMediaSourceHolders.clear(); + } + + @Override + protected synchronized void releaseSourceInternal() { super.releaseSourceInternal(); mediaSourceHolders.clear(); + enabledMediaSourceHolders.clear(); mediaSourceByUid.clear(); shuffleOrder = shuffleOrder.cloneAndClear(); if (playbackThreadHandler != null) { @@ -718,6 +740,11 @@ private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSour mediaSourceHolders.add(newIndex, newMediaSourceHolder); mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) { + enabledMediaSourceHolders.add(newMediaSourceHolder); + } else { + disableChildSource(newMediaSourceHolder); + } } private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { @@ -772,10 +799,27 @@ private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffs 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()) { + enabledMediaSourceHolders.remove(mediaSourceHolder); releaseChildSource(mediaSourceHolder); } } + private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { + enabledMediaSourceHolders.add(mediaSourceHolder); + enableChildSource(mediaSourceHolder); + } + + private void disableUnusedMediaSources() { + Iterator 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 c3219a03c1c..5ee980d01f7 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 @@ default Object getTag() { } /** - * 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 @@ default Object getTag() { /** * 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 @@ default Object getTag() { /** * 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 30083cb8498..974e033b670 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 @@ private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format form */ 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 f3936e5dc2f..455ee6c034d 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 @@ private DecoderQueryException(Throwable cause) { 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 static Pair getCodecProfileAndLevel(@Nullable String co 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 static Pair getCodecProfileAndLevel(@Nullable String co 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 @@ private static Pair getVp9ProfileAndLevel(String codec, String 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 boolean equals(@Nullable Object obj) { 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 a84c6f5d7bd..05d92e07834 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 void getCodecProfileAndLevel_handlesDolbyVisionCodecString() { 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 78b3a35549d..89160f45f3d 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 @@ public synchronized int peekNext(Format downstreamFormat) { 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 9d66a6f800e..3382f01e8a1 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 73107aa98e6..f380af968c3 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 @@ public int getCurrentAdIndexInAdGroup() { 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 0fc7242279d..38cdb57fc84 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 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) 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 bc1ea7b1e1c..2733df7ba6f 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 @@ 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 e47d8af3812..0dacd4df305 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 @@ public MediaPeriod enqueueNextMediaPeriod( 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 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { } long startPositionUs; + long contentPositionUs; int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; Object nextPeriodUid = period.uid; @@ -568,6 +571,7 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { // 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 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { 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 @@ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { 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 1eedae08b6b..669f41ca134 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 @@ /** * 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 61b84184116..39046b52cee 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 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 void run(SimpleExoPlayer player) { 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 f7c6694409a..b61c5f9b2c8 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 ExoPlayerTestRunner start() { 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 f06c9da048f..df01df1708b 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 static Format createVideoSampleFormat( // 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 static Format createImageSampleFormat( // 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 9e7da87766a..ee731cbc09f 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 interface EventListener { } - /** Use {@link ProgressiveMediaSource.Factory} instead. */ + /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */ @Deprecated public static final class Factory implements MediaSourceFactory { @@ -221,6 +221,9 @@ public int[] getSupportedTypes() { } } + /** + * @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 07155ee2bc8..17fb4ad7a1f 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 final RequestProperties getDefaultRequestProperties() { 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 3382f01e8a1..a769ff5f0cd 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 03804fa577d..94a6e21c86a 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 DataSchemeDataSource() { 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 long open(DataSpec dataSpec) throws IOException { 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 int read(byte[] buffer, int offset, int readLength) { 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 Uri getUri() { } @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 2df9a608e9d..8cb142f05da 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 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 @@ @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 void setUp() { @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 void testPartialReads() throws IOException { 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 void testMalformedData() { } 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 a769ff5f0cd..71948827584 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 949bd178eaa..b8dd40f8bdc 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 @@ protected static int getFormatLanguageScore(Format format, @Nullable String lang 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 32e9c32a53d..a8aa2c630bc 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.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 static void writeBoolean(Parcel parcel, boolean value) { 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 @@ private static String getLocaleLanguageTagV21(Locale locale) { } } + 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 9abec0cd8f2..f85ee37c079 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 void testInflate() { @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 void testNormalizeLanguageCodeV21() { @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 095739271e3..254a2b2bd16 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 void testPlaylistWithClosedCaption() throws IOException { 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 3310b0dc8b4..acaf798b419 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 int getMaxSupportedInstances() { * @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 boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQue } /** - * 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 boolean isCodecSupported(String codec) { return true; } } - logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); return false; } @@ -279,8 +279,7 @@ public boolean isSeamlessAdaptationSupported(Format format) { 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 boolean isSeamlessAdaptationSupported( } // 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 455ee6c034d..df5ca059728 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 static int maxH264DecodableFrameSize() throws DecoderQueryException { } /** - * 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 d9d81cf6d43..6e3114d1b10 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 @@ private static List getDecoderInfos( 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 @@ protected MediaFormat getMediaFormat( 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 05d92e07834..c485ff49f6c 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 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 void getCodecProfileAndLevel_handlesFullAv1CodecString() { @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 89160f45f3d..09bc438f905 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 @@ public static final class SampleExtrasHolder { private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; + private Format upstreamCommittedFormat; private int upstreamSourceId; public SampleMetadataQueue() { @@ -96,6 +97,7 @@ public void reset(boolean resetUpstreamFormat) { largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; isLastSampleQueued = false; + upstreamCommittedFormat = null; if (resetUpstreamFormat) { upstreamFormat = null; upstreamFormatRequired = true; @@ -227,7 +229,7 @@ public synchronized int peekNext(Format downstreamFormat) { 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 @@ public synchronized int read( 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 @@ public synchronized int read( } 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 @@ public synchronized boolean format(Format format) { } 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 @@ public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlag 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 bfc6bb52c98..6812e08ef74 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 void testReadWithoutWrite() { } @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 void testReadFormatDeduplicated() { 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 54c78eb33da..02c676dfdf3 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 synchronized DefaultExtractorsFactory setConstantBitrateSeekingEnabled( /** * 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 synchronized Extractor[] createExtractors() { : 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 0a0755327c5..2f46744ea09 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 int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce 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 4db02e0d83a..2e8dcd952b5 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 int read(ExtractorInput input, PositionHolder seekPosition) 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 d1e3217e308..3dd88fbb518 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 void init(ExtractorOutput output) { 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 4527e41f344..060f7fb81d6 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 void testSeeking_handlesRandomSeeks_extractsCorrectSamples() // 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 25e2a336ff1..feb14d1adb0 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 void testSample() throws Exception { @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 71948827584..30098e01dfb 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 a8aa2c630bc..e700fc67511 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 static void writeBoolean(Parcel parcel, boolean value) { 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 @@ private static void getDisplaySizeV16(Display display, Point outSize) { } 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 f85ee37c079..5a13ed0dd85 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 void testInflate() { @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 void testNormalizeLanguageCodeV21() { @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 | 21 ++--- .../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, 174 insertions(+), 92 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 bcec6426d67..410dffd558a 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 @@ 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 @@ public long getPositionUs() { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (rendererClock != null) { - playbackParameters = rendererClock.setPlaybackParameters(playbackParameters); + rendererClock.setPlaybackParameters(playbackParameters); + playbackParameters = rendererClock.getPlaybackParameters(); } standaloneMediaClock.setPlaybackParameters(playbackParameters); - listener.onPlaybackParametersChanged(playbackParameters); - return playbackParameters; } @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 f380af968c3..3eed66402d1 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 @@ 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 @@ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameter 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 @@ public Timeline getCurrentTimeline() { /* 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 @@ public Timeline getCurrentTimeline() { } } + 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 38cdb57fc84..738a30fad17 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 @@ public void onTrackSelectionsInvalidated() { @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 @@ public boolean handleMessage(Message msg) { 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 @@ private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackExce private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { mediaClock.setPlaybackParameters(playbackParameters); + sendPlaybackParametersChangedInternal( + mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true); } private void setSeekParametersInternal(SeekParameters seekParameters) { @@ -1663,9 +1664,13 @@ private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) { 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 @@ private void updateLoadControlTrackSelection( 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 4e062dcb5e7..eed59876f92 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 void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { /** * 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 393380453c5..f2458a7471d 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 @@ boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) 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 e3f753958e7..b4e00589822 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 boolean hasPendingData() { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (configuration != null && !configuration.canApplyPlaybackParameters) { this.playbackParameters = PlaybackParameters.DEFAULT; - return this.playbackParameters; - } - PlaybackParameters lastSetPlaybackParameters = - afterDrainPlaybackParameters != null - ? afterDrainPlaybackParameters - : !playbackParametersCheckpoints.isEmpty() - ? playbackParametersCheckpoints.getLast().playbackParameters - : this.playbackParameters; + return; + } + 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 PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParam 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 7e889097bce..b965f4ef688 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 long getPositionUs() { } @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 ef0207517ac..b17fa75181f 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 long getPositionUs() { } @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 a10298e4561..e9f08a35c96 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 b1f53416fbd..e5f9aa645fe 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 long getPositionUs() { } @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 be0f7f55c7b..c42edb32ae4 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 @@ 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 void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParame @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 void standaloneSetPlaybackParameters_shouldApplyNewPlaybackSpeed() { @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 void rendererClockSetPlaybackParameters_getPlaybackParametersShouldReturn 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 void rendererClockSetPlaybackParametersOverwrite_getParametersShouldRetur 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 long getPositionUs() { } @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 39046b52cee..d8ba3bcbda0 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 long getPositionUs() { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return PlaybackParameters.DEFAULT; - } + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} @Override public PlaybackParameters getPlaybackParameters() { @@ -2641,6 +2639,95 @@ public void run(SimpleExoPlayer player) { 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 df5ca059728..6a967a359b6 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 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 static Pair getCodecProfileAndLevel(Format format) { 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 @@ private static Pair getVp9ProfileAndLevel(String codec, String 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 @@ private static Pair getAv1ProfileAndLevel(String codec, String 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 @@ private static Pair getAv1ProfileAndLevel(String codec, String 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 c485ff49f6c..e8d65255c38 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 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 void getCodecProfileAndLevel_handlesAv1ProfileMain10CodecString() { 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 @@ private static void assertCodecProfileAndLevelForCodecsString( /* 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 b5d282f8a7d..b830fa5b785 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 @@ protected Pair selectTextTrack( 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 @@ protected static final class TextTrackScore implements Comparable 0 + || (parameters.selectUndeterminedTextLanguage && trackHasNoLanguage); + hasSelectedAudioLanguageMatch = (selectedAudioLanguageScore > 0) + || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)); + isWithinConstraints = + (hasLanguageMatch || isDefault || (isForced && hasSelectedAudioLanguageMatch)); } /** @@ -2546,34 +2552,26 @@ public int compareTo(@NonNull TextTrackScore other) { 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 df01df1708b..b2bd20f0fe6 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 Format copyWithFrameRate(float frameRate) { } 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 b0b10d4e987..365a48cadf2 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.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 DecryptableSampleQueueReader(SampleQueue upstream, DrmSessionManager s 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 @@ private void onFormat(Format format, FormatHolder outputFormatHolder) { 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 079852c4d47..360a7d6f724 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 @@ public TrackOutput track(int id, int type) { 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 @@ private static DummyTrackOutput createDummyTrackOutput(int id, int type) { 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 255799c0262..60386dcc3cb 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 Metadata peekId3Data( 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 2f46744ea09..b1d15b7189d 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 Ac3Extractor() { @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 2e8dcd952b5..205d71e16e2 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 @@ 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 Ac4Extractor() { @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 3dd88fbb518..381f19809bf 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 AdtsExtractor(@Flags int flags) { 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 int read(ExtractorInput input, PositionHolder seekPosition) 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 f936fb9e437..77ec48d0a75 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 void consume(ParsableByteArray data) { 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 void consume(ParsableByteArray data) { 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 249223bff56..1f60224b284 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 @@ private MediaSource buildMediaSource( .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 39b49da402a..e21827557a1 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.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 HlsMediaPeriod( HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Allocator allocator, @@ -114,6 +120,7 @@ public HlsMediaPeriod( 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 @@ private HlsSampleStreamWrapper buildSampleStreamWrapper( 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 12c6a8ee728..877b6d486ea 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 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 static final class Factory implements MediaSourceFactory { @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 Factory(HlsDataSourceFactory hlsDataSourceFactory) { playlistParserFactory = new DefaultHlsPlaylistParserFactory(); playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY; extractorFactory = HlsExtractorFactory.DEFAULT; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } @@ -127,6 +131,20 @@ public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { 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 HlsMediaSource createMediaSource(Uri playlistUri) { hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), @@ -297,6 +316,7 @@ public int[] getSupportedTypes() { 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 @@ private HlsMediaSource( HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, @@ -319,6 +340,7 @@ private HlsMediaSource( 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 cf879e91c6a..c820038b80b 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 @@ public void maybeThrowError() throws 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 360a7d6f724..c8c1b8f566b 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.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 @@ public interface Callback extends SequenceableLoader.Callback drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; @@ -107,6 +111,7 @@ public interface Callback extends SequenceableLoader.Callback overridingDrmInitData; private SampleQueue[] sampleQueues; + private DecryptableSampleQueueReader[] sampleQueueReaders; private int[] sampleQueueTrackIds; private boolean audioSampleQueueMappingDone; private int audioSampleQueueIndex; @@ -154,6 +159,8 @@ public interface Callback extends SequenceableLoader.Callback drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.trackType = trackType; @@ -173,6 +181,7 @@ public HlsSampleStreamWrapper( 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 @@ public HlsSampleStreamWrapper( 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 @@ public void continuePreparing() { 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 @@ public void release() { for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(this); handler.removeCallbacksAndMessages(null); @@ -448,6 +461,9 @@ public void release() { @Override public void onLoaderReleased() { resetSampleQueues(); + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } public void setIsTimestampMaster(boolean isTimestampMaster) { @@ -461,7 +477,12 @@ public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) { // 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 @@ && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) { } 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 @@ && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) { : 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 @@ public TrackOutput track(int id, int type) { 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 @@ private void buildTracksFromSampleStreams() { 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 f389944670e..93b8be3346e 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 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 void getSteamKeys_isCompatibleWithHlsMasterPlaylistFilter() { 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 83145d04b0a..d25fff51042 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 @@ public void release() { 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 @@ public void onLoaderReleased() { 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 1f60224b284..40b1a949912 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 @@ private void initializePlayer() { 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 d8ba3bcbda0..d5b0b2c6677 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.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 void run(SimpleExoPlayer player) { 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 void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackG 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 03a54947cfd..85e60f27969 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 856b0b1235c..dbfdd833f65 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 c17c0a62ab0..2adfb286322 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 @@ 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 @@ private View buildSampleListView() { 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 adb8e590709..79f36f46d7a 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 @@ 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.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 Builder setTitle(String title) { 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 Builder setMedia(UriBundle media) { return this; } - /** See {@link MediaItem#attachment}. */ - public Builder setAttachment(Object attachment) { - this.attachment = attachment; - return this; - } - /** See {@link MediaItem#drmSchemes}. */ public Builder setDrmSchemes(List drmSchemes) { this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes)); return this; } - /** See {@link MediaItem#startPositionUs}. */ - public Builder setStartPositionUs(long startPositionUs) { - this.startPositionUs = startPositionUs; - return this; - } - - /** See {@link MediaItem#endPositionUs}. */ - public Builder setEndPositionUs(long endPositionUs) { - Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE); - this.endPositionUs = endPositionUs; - return this; - } - /** See {@link MediaItem#mimeType}. */ public Builder setMimeType(String mimeType) { this.mimeType = mimeType; return this; } - /** - * Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the - * {@link MediaItem}. - */ - public MediaItem buildAndClear() { - MediaItem item = build(); - clearInternal(); - return item; - } - - /** Returns the builder to default values. */ - public Builder clear() { - clearInternal(); - 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 int hashCode() { } } - /** - * 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 boolean equals(@Nullable Object other) { 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 9cdc073b067..d21e57efd1b 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 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 @@ @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 void buildMediaItem_assertDefaultValues() { assertDefaultValues(new MediaItem.Builder().build()); } - @Test - public void buildAndClear_assertDefaultValues() { - MediaItem.Builder builder = new MediaItem.Builder(); - builder - .setMedia("http://example.com") - .setTitle("title") - .setMimeType(MimeTypes.AUDIO_MP4) - .setStartPositionUs(3) - .setEndPositionUs(4) - .buildAndClear(); - assertDefaultValues(builder.build()); - } - @Test public void equals_withEqualDrmSchemes_returnsTrue() { - MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem.Builder builder1 = new MediaItem.Builder(); MediaItem mediaItem1 = - builder - .setUuid(new UUID(0, 1)) - .setMedia("www.google.com") - .setDrmSchemes(createDummyDrmSchemes(1)) - .buildAndClear(); + builder1.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 fe74f5c59c4..b8b95989892 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.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 2d65b64f366..44bcdfd495a 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 interface PlayerControl { 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 @@ private boolean willPauseWhenDucked() { * @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 5a9070ecf25..176c7866826 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 c1c0c5cbc7a..56eebfbee4f 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 @@ protected static final class TextTrackScore implements Comparable 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 int compareTo(TextTrackScore other) { 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.isForcedAndSelectedAudioLanguage != other.isForcedAndSelectedAudioLanguage) { - return this.isForcedAndSelectedAudioLanguage ? 1 : -1; + if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { + return this.hasPreferredIsForcedFlag ? 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 66a47074969..81af551b685 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 Builder() { * 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 Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage return this; } - // Text - /** * See {@link TrackSelectionParameters#preferredTextLanguage}. * @@ -117,15 +111,14 @@ public TrackSelectionParameters build() { 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 TrackSelectionParameters build() { } /* 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 boolean equals(@Nullable Object obj) { } 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 boolean equals(@Nullable Object obj) { @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 int describeContents() { @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 3672c304ccb..4ebe97313b1 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 @@ protected abstract void onChildSourceInfoRefreshed( /** * 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 @@ protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) { 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 ForwardingEventListener(T id) { @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 1fca8249103..d9dd83de4f6 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 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 MaskingMediaPeriod createPeriod( // 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 MaskingMediaPeriod createPeriod( @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 @@ protected void onChildSourceInfoRefreshed( 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 @@ protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( 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 0dacd4df305..2597cd9b3fd 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 @@ public void clear(boolean keepFrontPeriodUid) { 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 de0f1773422..091696f8bfc 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 @@ private static final class MediaPeriodQueueTracker { 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 MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { /** 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 void onTimelineChanged(Timeline timeline) { readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported start of seek. */ @@ -806,7 +807,7 @@ public void onSeekStarted() { /** 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 void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { 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 boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { 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 void onReadingStarted(MediaPeriodId mediaPeriodId) { 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 6a967a359b6..e8fead61ae7 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 boolean equals(@Nullable Object obj) { 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 2d5a5f0ccfd..91ea0c92e2d 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 2adfb286322..1a7f28cd77e 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.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 @@ private View buildSampleListView() { 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 void onBindViewHolder(QueueItemViewHolder holder, int position) { 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 void onClick(View v) { } } - 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 d2a1ca08608..b877ac75939 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.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 @@ private void maybeSetCurrentItemAndNotify(int currentItemIndex) { } 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 @@ private static MediaSource buildMediaSource(MediaItem item) { 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 @@ 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()) + 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 @@ private static MediaQueueItem buildMediaQueueItem(MediaItem item) { } @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 79f36f46d7a..7ac0da7078b 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,52 +17,37 @@ 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#title}. */ - public Builder setTitle(String title) { - this.title = title; - 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; + /** See {@link MediaItem#uri}. */ + public Builder setUri(Uri uri) { + this.uri = uri; return this; } - /** See {@link MediaItem#drmSchemes}. */ - public Builder setDrmSchemes(List drmSchemes) { - this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes)); + /** See {@link MediaItem#title}. */ + public Builder setTitle(String title) { + this.title = title; return this; } @@ -72,171 +57,119 @@ public Builder setMimeType(String mimeType) { return this; } - /** Returns a new {@link MediaItem} instance with the current builder values. */ - public MediaItem build() { - return new MediaItem( - title, - media, - drmSchemes, - mimeType); - } - } - - /** 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); + /** See {@link MediaItem#drmConfiguration}. */ + public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) { + this.drmConfiguration = drmConfiguration; + return this; } - @Override - public int hashCode() { - int result = uri.hashCode(); - result = 31 * result + requestHeaders.hashCode(); - return result; + /** Returns a new {@link MediaItem} instance with the current builder values. */ + public MediaItem build() { + Assertions.checkNotNull(uri); + return new MediaItem(uri, title, mimeType, drmConfiguration); } } - /** - * 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 d21e57efd1b..7b410a8fbcd 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 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 void buildMediaItem_doesNotChangeState() { 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 void buildMediaItem_doesNotChangeState() { 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 void equals_withEqualDrmSchemes_returnsTrue() { 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 6e3114d1b10..24524d057d1 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 @@ protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { 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 0bf6baa4d0b..6cd46bb7052 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 @@ 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 @@ public void flush() { 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 0d938d33f48..bd32e5ee6f2 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 void queueInput(ByteBuffer inputBuffer) { 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 ByteBuffer getOutput() { @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 176c7866826..747436a69de 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 c8136ea4711..aba52817bc5 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 190f4de5a6c..f420f207679 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 | 60 +++++-- 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, 334 insertions(+), 27 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 151875c2c5e..9f79f09117d 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 @@ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedExcept 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 4ba071e1cae..4ccd24781bc 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"); - - 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); + 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; } 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 b2d074252dd..fafc2544826 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 f09a22e951b..f1d175b94fe 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 00000000000..fcf1fd6e585 --- /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 b1951cbc13e..9f44cdf393d 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 43fdda367e5..48680b50956 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 @@ 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 FlacStreamMetadata(byte[] data, int offset) { 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 FlacStreamMetadata(byte[] data, int offset) { * @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 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 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 00000000000..04a5b46e26e --- /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 868b28b0e1e..bb118e381a7 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 325d9b19f68..c556282ca2e 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 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 void parseVorbisComments() { 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 void parseVorbisComments() { 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 void parseVorbisCommentWithEqualsInValue() { 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 void parseInvalidVorbisComment() { 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 269c48c282a..4ac007fa550 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.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 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { } 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 | 164 ++++++++++-------- .../android/exoplayer2/MediaPeriodQueue.java | 5 + 2 files changed, 99 insertions(+), 70 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 738a30fad17..d356fe11d20 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,80 +1490,86 @@ private void updatePeriods() throws ExoPlaybackException, IOException { 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; } - // Advance the reading period if necessary. 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; } - 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 (!hasReadingPeriodFinishedReading()) { + return; } if (!readingPeriodHolder.getNext().prepared) { @@ -1576,18 +1582,18 @@ private void updatePeriods() throws ExoPlaybackException, IOException { 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 @@ private void updatePeriods() throws ExoPlaybackException, IOException { } } - 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 2597cd9b3fd..9c0dd80a10c 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 @@ public MediaPeriod enqueueNextMediaPeriod( * 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 @@ public MediaPeriodHolder getLoadingPeriod() { * 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 @@ public MediaPeriodHolder getPlayingPeriod() { * 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 @@ public MediaPeriodHolder getReadingPeriod() { * 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 @@ public MediaPeriodHolder advanceReadingPeriod() { * * @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 d356fe11d20..b6317941fb3 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 @@ private void updatePeriods() throws ExoPlaybackException, IOException { return; } maybeUpdateLoadingPeriod(); - maybeUpdatePlayingPeriod(); maybeUpdateReadingPeriod(); + maybeUpdatePlayingPeriod(); } private void maybeUpdateLoadingPeriod() throws IOException { @@ -1518,32 +1518,6 @@ private void maybeUpdateLoadingPeriod() throws IOException { } } - 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 @@ private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException } } + 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 | 113 ++++++++++++++--- .../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, 255 insertions(+), 127 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 747436a69de..7afe0a78cd6 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 b877ac75939..421269772c0 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 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.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 @@ public PlayerManager( 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 bd9c85da511..059f26b3744 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.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 @@ private void initializePlayer() { 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 e1e866bbee7..839ed304bde 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.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 interface Listener { 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 DownloadTracker( listeners = new CopyOnWriteArraySet<>(); downloads = new HashMap<>(); downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); downloadManager.addListener(new DownloadManagerListener()); loadDownloads(); } @@ -123,13 +126,13 @@ private DownloadHelper getDownloadHelper( 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 void onPrepared(DownloadHelper helper) { 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 void onPrepared(DownloadHelper helper) { @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 void onClick(DialogInterface dialog, int which) { 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 40b1a949912..d8bfe23674f 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 void onCreate(Bundle savedInstanceState) { 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 @@ private void initializePlayer() { 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 12ef68ee3cf..c10d6fdb273 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 TestPlaybackRunnable(Uri uri, Context context) { 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 7c6835db0b3..382ee38e063 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 TestPlaybackRunnable(Uri uri, Context context) { 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 5ebeca68d0e..9be1d9c0e52 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 TestPlaybackRunnable(Uri uri, Context context) { 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 59647feaa9a..956f22f7197 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 static SimpleExoPlayer newSimpleInstance( * @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 4b5bf3c8a4a..6952413129c 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 @@ */ 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,36 +134,70 @@ public interface Callback { private static final Constructor HLS_FACTORY_CONSTRUCTOR = getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + /** @deprecated Use {@link #forProgressive(Context, Uri)} */ + @Deprecated + @SuppressWarnings("deprecation") + public static DownloadHelper forProgressive(Uri uri) { + return forProgressive(uri, /* cacheKey= */ null); + } + /** * Creates a {@link DownloadHelper} for progressive streams. * + * @param context Any {@link Context}. * @param uri A stream {@link Uri}. * @return A {@link DownloadHelper} for progressive streams. */ - public static DownloadHelper forProgressive(Uri uri) { - return forProgressive(uri, /* cacheKey= */ null); + 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_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(Uri uri, @Nullable String cacheKey) { + public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) { return new DownloadHelper( DownloadRequest.TYPE_PROGRESSIVE, uri, cacheKey, /* mediaSource= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS, + 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 static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) * @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 static DownloadHelper forDash( 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 static DownloadHelper forDash( * @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 static DownloadHelper forHls( 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 static DownloadHelper forHls( * @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 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 56eebfbee4f..cc1742bb31d 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 static final class ParametersBuilder extends TrackSelectionParameters.Bui 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 @@ private static SparseArray> cloneSelecti */ 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 static final class Parameters extends TrackSelectionParameters { 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 SelectionOverride[] newArray(int size) { 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 DefaultTrackSelector(BandwidthMeter bandwidthMeter) { 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 479936b82f6..111edc7af80 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 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.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 void setUp() { 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 void getTrackSelections_afterReplaceTrackSelections_returnsNewSelections( 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 void getTrackSelections_afterAddTrackSelections_returnsCombinedSelections // 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 void getDownloadRequest_createsDownloadRequest_withAllSelectedTracks() th // 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 e4501755240..4622dc17347 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.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 static void setUpBeforeClass() { 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 void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlayb /** 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 void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { /** 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 void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackExc } /** - * 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 void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListen * 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 void testSelectTracksSelectPreferredAudioLanguage() 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 void testSelectTracksSelectPreferredAudioLanguageOverSelectionFlag() 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 void testSelectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoS 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 void testSelectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage() 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 void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPre 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 void testTextTrackSelectionFlags() throws ExoPlaybackException { // 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 void testTextTrackSelectionFlags() throws ExoPlaybackException { 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 void testTextTrackSelectionFlags() throws ExoPlaybackException { 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 void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackExc 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 void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackExc 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 void testSelectPreferredTextTrackMultipleRenderers() throws Exception { 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 void testSelectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBi 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 void testSelectTracksWithinCapabilitiesAndForceHighestBitrateSelectHigher 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 void testSelectTracksWithMultipleAudioTracksWithMixedSampleRates() throws // 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 void testSelectTracksWithMultipleAudioTracksWithMixedMimeTypes() throws E // 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 void testSelectTracksWithMultipleAudioTracksWithMixedChannelCounts() thro // 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 void testSelectTracksWithMultipleAudioTracksWithMixedChannelCounts() thro // 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 void testSelectTracksWithMultipleAudioTracksWithMixedChannelCounts() thro // 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 void testSelectTracksWithMultipleAudioTracksWithMixedChannelCounts() thro // 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 void testSelectPreferredAudioTrackMultipleRenderers() throws Exception { 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 void testSelectTracksWithMultipleVideoTracksWithNonSeamlessAdaptiveness() // 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 void testSelectTracksWithMultipleVideoTracksWithNonSeamlessAdaptiveness() // 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 void testSelectTracksWithMultipleVideoTracksWithMixedMimeTypes() throws E // 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 int getTrackType() { } @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 int getTrackType() { } @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 73225f68c79..107bf7c790a 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 void staticDownloadHelperForDash_doesNotThrow() { 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 c7a8034ee7c..3c81074c254 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 void staticDownloadHelperForHls_doesNotThrow() { 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 4da08f76315..a103f89cec0 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 void staticDownloadHelperForSmoothStreaming_doesNotThrow() { 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 b2a49a31fee..e452e391d58 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.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 @@ protected TrackSelection.Definition[] selectAllTracks( 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 90f2294bfcf..3ebd47b7a68 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.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 @@ protected DrmSessionManager buildDrmSessionManager(String } 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 b61c5f9b2c8..9de7996d3c6 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 ExoPlayerTestRunner build(Context context) { 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 7afe0a78cd6..5279a246982 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 a5b78ef3f7f..c3eab689833 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 long selectTracks( @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 long readDiscontinuity() { @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 long seekToUs(long positionUs) { @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return positionUs; + return constrainSeekPosition(positionUs); } @Override @@ -171,6 +173,10 @@ public boolean continueLoading(long positionUs) { @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 SilenceSampleStream(long durationUs) { } 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 | 257 ++++++++++-------- .../android/exoplayer2/demo/Sample.java | 187 +++++++++++++ .../demo/SampleChooserActivity.java | 148 ++-------- demos/main/src/main/res/values/strings.xml | 2 + 4 files changed, 351 insertions(+), 243 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 d8bfe23674f..1e231dd45e9 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.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.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"; + // Activity extras. - public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; - 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"; + + // 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"; - 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"; + // 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"; - 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"; + // 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 void onVisibilityChange(int visibility) { 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,55 +379,125 @@ private void initializePlayer() { playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); + if (adsLoader != null) { + adsLoader.setPlayer(player); + } + } + boolean haveStartPosition = startWindow != C.INDEX_UNSET; + if (haveStartPosition) { + player.seekTo(startWindow, startPosition); + } + player.prepare(mediaSource, !haveStartPosition, false); + updateButtonVisibility(); + } + + @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}; - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i], drmSessionManager); + 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; } - 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 (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, Uri.parse(adTagUriString)); + MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri); if (adsMediaSource != null) { mediaSource = adsMediaSource; } else { showToast(R.string.ima_not_loaded); } - } else { - releaseAdsLoader(); } + } else { + releaseAdsLoader(); } - boolean haveStartPosition = startWindow != C.INDEX_UNSET; - if (haveStartPosition) { - player.seekTo(startWindow, startPosition); - } - player.prepare(mediaSource, !haveStartPosition, false); - updateButtonVisibility(); - } - private MediaSource buildMediaSource(Uri uri) { - return buildMediaSource( - uri, - /* overrideExtension= */ null, - /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager()); + 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 @@ private DefaultDrmSessionManager buildDrmSessionManagerV18 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 @@ private void releasePlayer() { 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 @@ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) // 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 Pair getErrorMessage(ExoPlaybackException e) { 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 00000000000..4497b9a9849 --- /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 7245de01c64..09fa62e51af 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.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 @@ private void onSampleGroups(final List groups, boolean sawError) { 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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 SampleGroup(String title) { } } - - 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 0729da2fc6a..f74ce8c0760 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 421269772c0..44d9a60ff22 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 @@ 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.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.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 @@ interface Listener { 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 @@ public PlayerManager( mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; concatenatingMediaSource = new ConcatenatingMediaSource(); + mediaItemConverter = new DefaultMediaItemConverter(); exoPlayer = ExoPlayerFactory.newSimpleInstance(context); exoPlayer.addListener(this); @@ -133,7 +132,7 @@ public void addItem(MediaItem item) { mediaQueue.add(item); concatenatingMediaSource.addMediaSource(buildMediaSource(item)); if (currentPlayer == castPlayer) { - castPlayer.addItems(buildMediaQueueItem(item)); + castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item)); } } @@ -344,7 +343,7 @@ private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenRead 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 @@ private static MediaSource buildMediaSource(MediaItem item) { 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 00000000000..c8db958d039 --- /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 00000000000..3cb2540ad8a --- /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 1a7f28cd77e..244025f90d1 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 @@ 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 @@ private static final class SampleListAdapter extends ArrayAdapter { 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 | 128 +++++++++++++++--- .../ext/cast/MediaItemConverter.java | 8 +- .../cast/DefaultMediaItemConverterTest.java | 66 +++++++++ 3 files changed, 180 insertions(+), 22 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 c8db958d039..098803a512c 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(); } + // Deserialization. + + private static MediaItem getMediaItem(JSONObject customData) { + try { + JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM); + MediaItem.Builder builder = new MediaItem.Builder(); + builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI))); + if (mediaItemJson.has(KEY_TITLE)) { + builder.setTitle(mediaItemJson.getString(KEY_TITLE)); + } + if (mediaItemJson.has(KEY_MIME_TYPE)) { + builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE)); + } + if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) { + builder.setDrmConfiguration( + getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION))); + } + return builder.build(); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException { + UUID uuid = UUID.fromString(json.getString(KEY_UUID)); + Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI)); + JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS); + HashMap requestHeaders = new HashMap<>(); + for (Iterator iterator = requestHeadersJson.keys(); iterator.hasNext(); ) { + String key = iterator.next(); + requestHeaders.put(key, requestHeadersJson.getString(key)); + } + return new DrmConfiguration(uuid, licenseUri, requestHeaders); + } + + // Serialization. + private static JSONObject getCustomData(MediaItem item) { - JSONObject customData = new JSONObject(); + 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; + } - MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration; + 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 @@ private static JSONObject getCustomData(MediaItem item) { } 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 3cb2540ad8a..23633aa4d2f 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 00000000000..cf9b9d3496e --- /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 9f79f09117d..cd91b062888 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 @@ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedExcept 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 4ccd24781bc..9cc611559a5 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 fafc2544826..b9e5cace718 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 f1d175b94fe..9c6452c1606 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 fcf1fd6e585..dc280be9ffa 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 boolean equals(@Nullable Object obj) { 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 48680b50956..e7851aa0a4a 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 FlacStreamMetadata(byte[] data, int offset) { 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 FlacStreamMetadata(byte[] data, int offset) { * @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 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 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 04a5b46e26e..4263103eeb3 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 @@ /** 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 c556282ca2e..3988e5e45eb 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 void parseVorbisComments() { 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 void parseEmptyVorbisComments() { 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 void parseVorbisCommentWithEqualsInValue() { 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 void parseInvalidVorbisComment() { 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 4ac007fa550..1abdd33bb29 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 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { 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 bd768686250..a6f0c3966ac 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 669a0e7bb41..8dfea1e5116 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 synchronized void addMediaSources( * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. + * @return The removed {@link MediaSource}. */ - public synchronized void removeMediaSource(int index) { + public synchronized MediaSource removeMediaSource(int index) { + MediaSource removedMediaSource = getMediaSource(index); removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null); + return removedMediaSource; } /** @@ -282,10 +285,13 @@ public synchronized void removeMediaSource(int index) { * @param handler The {@link Handler} to run {@code onCompletionAction}. * @param onCompletionAction A {@link Runnable} which is executed immediately after the media * source has been removed from the playlist. + * @return The removed {@link MediaSource}. */ - public synchronized void removeMediaSource( + public synchronized MediaSource removeMediaSource( int index, Handler handler, Runnable onCompletionAction) { + MediaSource removedMediaSource = getMediaSource(index); removePublicMediaSources(index, index + 1, handler, onCompletionAction); + return removedMediaSource; } /** From 39d5867c978e1c4e117aacbbbc4d378457fa4773 Mon Sep 17 00:00:00 2001 From: tonihei 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 ca8a0b12f97..c5d22c15cb9 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 static class Factory implements TrackSelection.Factory { 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 final void experimental_setTrackBitrateEstimator( 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 final void experimental_enableBlockFixedTrackSelectionBandwidth() { 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 final void experimental_enableBlockFixedTrackSelectionBandwidth() { } } } - 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 final void experimental_enableBlockFixedTrackSelectionBandwidth() { * @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 AdaptiveTrackSelection(TrackGroup group, int[] tracks, 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 AdaptiveTrackSelection(TrackGroup group, int[] tracks, * @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 AdaptiveTrackSelection( TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + long reservedBandwidth, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, @@ -391,7 +388,7 @@ public AdaptiveTrackSelection( this( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -445,18 +442,6 @@ public void experimental_setTrackBitrateEstimator(TrackBitrateEstimator trackBit 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 @@ private static final class DefaultBandwidthProvider implements BandwidthProvider 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 long getAllocatedBandwidth() { 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 91e7393fe7c..456f7f7107b 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 @@ private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIn 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 @@ private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDe 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 @@ private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferRee 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 410dffd558a..1971a4cefc3 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 @@ public interface PlaybackParameterListener { 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 @@ public interface PlaybackParameterListener { */ 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 @@ public void stop() { * @param positionUs The position to set in microseconds. */ public void resetPosition(long positionUs) { - standaloneMediaClock.resetPosition(positionUs); + standaloneClock.resetPosition(positionUs); } /** @@ -99,8 +104,7 @@ public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException { } this.rendererClock = rendererMediaClock; this.rendererClockSource = renderer; - rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters()); - ensureSynced(); + rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters()); } } @@ -114,30 +118,25 @@ public void onRendererDisabled(Renderer renderer) { 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 @@ public void setPlaybackParameters(PlaybackParameters playbackParameters) { 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 b6317941fb3..488d002ab25 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 @@ private void updatePlaybackPositions() throws ExoPlaybackException { 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 c42edb32ae4..b6e3d7a6485 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 void initMediaClockWithFakeClock() { @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 void standaloneResetPosition_shouldNotStartClock() throws Exception { @Test public void standaloneStart_shouldStartClock() throws Exception { mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -98,7 +99,7 @@ public void standaloneStartStopStart_shouldRestartClock() throws Exception { mediaClock.start(); mediaClock.stop(); mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -130,7 +131,7 @@ public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackSpeed() { 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 void enableRendererMediaClockWithFixedParameters_shouldTriggerCallback() 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 void enableRendererMediaClockWithFixedButSamePlaybackParameters_shouldNot FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @@ -183,7 +186,9 @@ public void disableRendererMediaClock_shouldKeepPlaybackParameters() 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 void rendererClockSetPlaybackParameters_getPlaybackParametersShouldReturn 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 void rendererClockSetPlaybackParameters_shouldNotTriggerCallback() 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 void rendererClockSetPlaybackParametersOverwrite_getParametersShouldRetur 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 void enableRendererMediaClock_usesRendererClockPosition() throws ExoPlayb 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 void resetPositionWhileUsingRendererMediaClock_shouldHaveNoEffect() 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 void disableRendererMediaClock_standaloneShouldBeSynced() throws ExoPlayb 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 void rendererNotReadyAndReadStreamToEnd_shouldFallbackToStandaloneClock() /* 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 void rendererEnded_shouldFallbackToStandaloneClock() /* isEnded= */ true, /* hasReadStreamToEnd= */ true); mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -302,7 +325,8 @@ public void staleDisableRendererClock_shouldNotThrow() 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 void enableSameRendererClockTwice_shouldNotThrow() 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 void enableOtherRendererClock_shouldThrow() } 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 d7a1c753028..0d5a168197d 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 @@ int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) * 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 aba52817bc5..b1c2c636c7b 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 00000000000..65c0ce080e7 --- /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 f532ae0e6a5..ecb81c44503 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 00000000000..61450c9cfd5 --- /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 00000000000..bd7a70eba03 --- /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 7a6a7e346fe..d76d3f37eab 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 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) 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 8f13cfaa116..a448934359d 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 int read(ExtractorInput input, PositionHolder seekPosition) /* 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 c83662ee83e..9700760c49e 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 @@ public void resetSeeking() { * 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 @@ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedExcepti * @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 @@ long readGranuleOfLastPage(ExtractorInput input) throws IOException, Interrupted * 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 ff32ae34629..c7fb3ff6a25 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 @@ 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 @@ public void reset() { * 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 68d252e318b..d3114f9b694 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 int read(ExtractorInput input, PositionHolder seekPosition) 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 c60117be607..c7858dcd962 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 @@ @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 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +58,33 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i /** * 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 d76d3f37eab..839a9e3d5c2 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 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) // 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 5279a246982..5bb858a2eb4 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 956f22f7197..9168f1bd765 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 static SimpleExoPlayer newSimpleInstance( loadControl, drmSessionManager, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), Util.getLooper()); } @@ -257,8 +257,8 @@ public static SimpleExoPlayer newSimpleInstance( * @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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( trackSelector, loadControl, drmSessionManager, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), looper); } @@ -315,8 +315,8 @@ public static SimpleExoPlayer newSimpleInstance( * @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 static SimpleExoPlayer newSimpleInstance( TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { return newSimpleInstance( context, @@ -335,7 +335,7 @@ public static SimpleExoPlayer newSimpleInstance( loadControl, drmSessionManager, getDefaultBandwidthMeter(context), - analyticsCollectorFactory, + analyticsCollector, looper); } @@ -348,8 +348,8 @@ public static SimpleExoPlayer newSimpleInstance( * @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 static SimpleExoPlayer newSimpleInstance( LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { return new SimpleExoPlayer( context, @@ -369,7 +369,7 @@ public static SimpleExoPlayer newSimpleInstance( 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 a782255cb87..8913fbdaba1 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 @@ protected SimpleExoPlayer( loadControl, drmSessionManager, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), looper); } @@ -156,8 +156,8 @@ protected SimpleExoPlayer( * @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 @@ protected SimpleExoPlayer( LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { this( context, @@ -177,7 +177,7 @@ protected SimpleExoPlayer( loadControl, drmSessionManager, bandwidthMeter, - analyticsCollectorFactory, + analyticsCollector, Clock.DEFAULT, looper); } @@ -190,8 +190,8 @@ protected SimpleExoPlayer( * @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 @@ protected SimpleExoPlayer( 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 @@ protected SimpleExoPlayer( // 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 091696f8bfc..825424ae043 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 AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Cloc 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 9de7996d3c6..7db1987d5bc 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 TestSimpleExoPlayer( 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 a448934359d..ecff963271c 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 int read(ExtractorInput input, PositionHolder seekPosition) /* 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 56f5fd2d099..34301742e50 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.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 @@ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybac * * @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 0e13e826300..544259ffc0e 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.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 @@ public VpxDecoder( 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 @@ public void setOutputMode(int outputMode) { } @Override - protected VpxInputBuffer createInputBuffer() { - return new VpxInputBuffer(); + protected VideoDecoderInputBuffer createInputBuffer() { + return new VideoDecoderInputBuffer(); } @Override @@ -123,7 +122,7 @@ protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { @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 fcae9dc6bc1..76742a8691a 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 e8fead61ae7..46bc448a4a8 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 @@ private static String getCodecSupportedType( 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 @@ private static String getCodecSupportedType( || "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 | 108 ++++++++---------- 1 file changed, 48 insertions(+), 60 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 9700760c49e..a4aa6b8dd5e 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.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 @@ public long getNextSeekPosition(long targetGranule, ExtractorInput input) return -(pageHeader.granulePosition + 2); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; - } - 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); + /** + * 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; } /** @@ -266,8 +260,7 @@ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedExcepti * @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 @@ long readGranuleOfLastPage(ExtractorInput input) throws IOException, Interrupted 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); + private final class OggSeekMap implements SeekMap { + + @Override + public boolean isSeekable() { + return true; } - input.resetPeekPosition(); - return currentGranule; - } + @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 a4aa6b8dd5e..308547e5108 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 @@ public long read(ExtractorInput input) throws IOException, InterruptedException } @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 d4c2bbb485d..4efd5c5e111 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 @@ public long read(ExtractorInput input) throws IOException, InterruptedException } @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 aa88e5bf899..e4c3a163e6b 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 @@ 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 e459ad1e584..35a07fcf49d 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 @@ final void seek(long position, long timeUs) { 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 @@ protected void onSeekEnd(long currentGranule) { 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 12c26ca2ecc..c78b02aa5b3 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 @@ public int getSampleRate() { 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 ea45374f866..b3c26246e6e 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 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType 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 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType 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 244025f90d1..d0e40990be0 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.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 void onQueuePositionChanged(int previousIndex, int newIndex) { } } + @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 44d9a60ff22..8b75eb0c748 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.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.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 @@ interface Listener { /** 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 @@ interface Listener { 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 @@ public PlayerManager( 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 @@ public boolean removeItem(MediaItem item) { 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 @@ public void release() { 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 @@ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reaso 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 @@ private void maybeSetCurrentItemAndNotify(int currentItemIndex) { } } - 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 2f0acd48085..69f0691630e 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 | 38 +++++++++++-------- .../extractor/wav/WavHeaderReader.java | 13 +++++-- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c37556c98c7..3c1748ea9de 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 d3114f9b694..91097c9e5b7 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 int read(ExtractorInput input, PositionHolder seekPosition) 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 c7858dcd962..6e3c5988a9a 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 @@ 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; - - public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, - int bitsPerSample, @C.PcmEncoding int encoding) { + /** 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) { this.numChannels = numChannels; this.sampleRateHz = sampleRateHz; this.averageBytesPerSecond = averageBytesPerSecond; @@ -51,6 +55,7 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i this.bitsPerSample = bitsPerSample; this.encoding = encoding; dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -59,11 +64,11 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i * 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 @@ public int getDataStartPosition() { } /** - * 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 @@ public boolean isSeekable() { @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 839a9e3d5c2..bbcb75aa2d9 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 @@ public static WavHeader peek(ExtractorInput input) throws IOException, Interrupt // 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 @@ public static void skipToData(ExtractorInput input, WavHeader wavHeader) // 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 | 116 ++++++++++-------- .../video/MediaCodecVideoRenderer.java | 6 +- 5 files changed, 91 insertions(+), 76 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c1748ea9de..9dd79d8c42a 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 b965f4ef688..6a29f316e18 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 @@ protected void configureCodec( 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 acaf798b419..d07def1894e 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 static MediaCodecInfo newPassthroughInstance(String name) { return new MediaCodecInfo( name, /* mimeType= */ null, + /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, /* forceDisableAdaptive= */ false, @@ -109,26 +117,8 @@ public static MediaCodecInfo newPassthroughInstance(String name) { * * @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 static MediaCodecInfo newInstance( 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 46bc448a4a8..cd4c4863ffb 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 static synchronized List getDecoderInfos( 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 static Pair getCodecProfileAndLevel(Format format) { // 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 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, 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 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, || (!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 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, } 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 @@ private static ArrayList getDecoderInfosInternal(CodecKey key, } /** - * 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; + } + + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + 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. - 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"; + 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 @@ private static String getCodecSupportedType( * @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 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S } // 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 @@ private static void applyWorkarounds(String mimeType, List decod // 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 24524d057d1..2ab7e613780 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 @@ protected void configureCodec( 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 @@ private static void configureTunnelingV21(MediaFormat mediaFormat, int tunneling * 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 @@ private static void configureTunnelingV21(MediaFormat mediaFormat, int tunneling @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 9dd79d8c42a..89acdfb9a46 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 87bb9920828..e454bd51c89 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 static boolean populateHeader(int headerData, MpegAudioHeader header) { } } - // 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 d4df3ffebae..96b0cd259c9 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 d4df3ffebae..96b0cd259c9 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 d4df3ffebae..96b0cd259c9 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 d4df3ffebae..96b0cd259c9 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 488d002ab25..4fe8da92c2f 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 @@ private void reselectTracksInternal() throws ExoPlaybackException { 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 @@ private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { 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 @@ private void maybeUpdateLoadingPeriod() throws IOException { } 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 a21afc4b51e..850d2b7d108 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 @@ 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 @@ * @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 @@ public MediaPeriodHolder( 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 @@ public long getNextLoadPositionUs() { 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 @@ public void continueLoading(long rendererPositionUs) { } /** - * 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 @@ public long applyTrackSelection( /** 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 @@ public MediaPeriodHolder getNext() { 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 @@ private void enableTrackSelectionsInResult() { } 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 @@ private void disassociateNoSampleRenderersWithEmptySampleStream( */ 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 9c0dd80a10c..0f279ba6d36 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.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 @@ public boolean shouldLoadNextMediaPeriod() { * @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 @@ public MediaPeriod enqueueNextMediaPeriod( 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 fc723134f72..9228f3af628 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 boolean isEquivalent(@Nullable TrackSelectorResult other) { /** * 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 def7f8552e9..14aa436be3a 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.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 @@ private void advanceReading() { 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 8de14629d3c..b2da9a7ff8a 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 00000000000..68108af636b --- /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 34301742e50..b6663ac3d75 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 LibvpxVideoRenderer( 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 boolean isReady() { 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 @@ protected void dropOutputBuffer(VpxOutputBuffer outputBuffer) { */ 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 @@ private void setOutput( 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 @@ private void setOutput( 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 @@ private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) } 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 544259ffc0e..93a4a2fc1fb 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 @@ /* 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 @@ 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 @@ public String getName() { /** * 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 @@ protected VpxOutputBuffer createOutputBuffer() { 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 30d7b8e92c1..de411089ab1 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 void release() { * 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 8ded5038b02..9ed5cb7e361 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 @@ private 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 a2546adfe4e..875f8b5d7ba 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 void testSeekBackAfterReadingAhead() throws Exception { .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 void testSeekBackAfterReadingAhead() throws Exception { 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 void testReprepareAfterError() throws Exception { .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 void testReprepareAfterError() throws Exception { 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 void testDynamicTimelineChange() throws Exception { 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 91ea0c92e2d..dacdbfe6160 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 @@ public static final List SAMPLES; static { - // App samples. ArrayList samples = new ArrayList<>(); // Clear content. @@ -59,6 +58,45 @@ .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 | 173 ++++++++---------- .../extractor/ogg/StreamReader.java | 2 +- .../extractor/ogg/DefaultOggSeekerTest.java | 109 +++++------ .../ogg/DefaultOggSeekerUtilMethodsTest.java | 95 +--------- 4 files changed, 128 insertions(+), 251 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 308547e5108..064bd5732d5 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 @@ 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 @@ /** * 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 @@ public long read(ExtractorInput input) throws IOException, InterruptedException 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 @@ public long read(ExtractorInput input) throws IOException, InterruptedException 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; - } + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; + } - long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + 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 @@ long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentG */ @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 @@ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedExcepti */ 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 @@ private boolean skipToNextPage(ExtractorInput input, long limit) 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 @@ public boolean isSeekable() { 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 35a07fcf49d..d2671125e4d 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 @@ private int readHeaders(ExtractorInput input) throws IOException, InterruptedExc 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 8d1818845dd..fba358ea513 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 @@ private void testSeeking(Random random) throws IOException, InterruptedException 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 @@ private void testSeeking(Random random) throws IOException, InterruptedException input.setPosition((int) nextSeekPosition); } - // Test granule 0 from file start - assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0); + // Test granule 0 from file start. + long granule = seekTo(input, oggSeeker, 0, 0); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - // Test granule 0 from file end - assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0); + // Test granule 0 from file end. + granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - { // Test last granule - long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - assertThat( - (testFile.lastGranule > currentGranule && position > input.getPosition()) - || (testFile.lastGranule == currentGranule && position == input.getPosition())) - .isTrue(); - } - - { // Test exact granule - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - assertThat( - (pageHeader.granulePosition > currentGranule && position > input.getPosition()) - || (pageHeader.granulePosition == currentGranule - && position == input.getPosition())) - .isTrue(); - } + // Test last granule. + granule = seekTo(input, oggSeeker, testFile.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(); 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 @@ private long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; + oggSeeker.startSeek(targetGranule); int count = 0; - oggSeeker.resetSeeking(); - - do { - input.setPosition((int) nextSeekPosition); - nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input); - + while (nextSeekPosition >= 0) { if (count++ > 100) { - fail("infinite loop?"); + fail("Seek failed to converge in 100 iterations"); } - } while (nextSeekPosition >= 0); - + input.setPosition((int) nextSeekPosition); + nextSeekPosition = oggSeeker.read(input); + } return -(nextSeekPosition + 2); } @@ -171,8 +153,7 @@ protected long preparePayload(ParsableByteArray packet) { } @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 d6691f50f88..25216022284 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 @@ private static void skipToNextPage(ExtractorInput extractorInput) 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 @@ private static void skipToNextPage(ExtractorInput extractorInput) } } - @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 void testReadGranuleOfLastPageAfterLastHeader() throws IOException, Inter assertReadGranuleOfLastPage(input, 60000); fail(); } catch (EOFException e) { - // ignored + // Ignored. } } @@ -216,7 +135,7 @@ public void testReadGranuleOfLastPageWithUnboundedLength() assertReadGranuleOfLastPage(input, 60000); fail(); } catch (IllegalArgumentException e) { - // ignored + // Ignored. } } @@ -224,9 +143,9 @@ private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) 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 @@ private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) 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 3937dabcafe..a933121bc5e 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 @@ private static FakeExtractorOutput assertOutput( 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 20f6f436b00..443ffdb12c8 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 @@ private FakeExtractorInput(byte[] data, boolean simulateUnknownLength, 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 c6543bd7a54..4022a0ccc17 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 void seekMap(SeekMap seekMap) { 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 fba358ea513..fd649f09248 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 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 @@ @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { @@ -55,6 +61,95 @@ public void testSeeking() throws IOException, InterruptedException { } } + @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 @@ private void testSeeking(Random random) throws IOException, InterruptedException } } - 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 25216022284..00000000000 --- 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 064bd5732d5..51ab94ba0ef 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 @@ /** 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 @@ public OggSeekMap createSeekMap() { @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 @@ private long getNextSeekPosition(ExtractorInput input) throws IOException, Inter 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 fd649f09248..8ba0be26a05 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 @@ private void testSeeking(Random random) throws IOException, InterruptedException /* 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 @@ private void testSeeking(Random random) throws IOException, InterruptedException 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 e5512dda363..38e4332b161 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 @@ 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 @@ public static OggTestFile generate(Random random, int pageCount) { 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 @@ public static OggTestFile generate(Random random, int pageCount) { 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 @@ public static OggTestFile generate(Random random, int pageCount) { } 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 bc538ead68f..1d0b459bf5b 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 e067789bc41..83e994c5e1c 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 0808ad6c447..7561857dca8 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 9065855a3fc..ea7564316f8 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 89acdfb9a46..bfdf9e733e1 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 457af80a8de..37d8fbbb995 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 33161b4121c..124555d9b5a 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 0bce1d4b82a..f58389d9d4a 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 83e994c5e1c..68a7494a3fe 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 7561857dca8..f7cc707fb41 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 ffecdcd16f1..15952b1860e 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 10b244cb39a..c67de276979 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 50acd6c0404..1031d6f4b76 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 2df9448d081..0ef9f281c90 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 c6f5a216ce3..ecaa78e25b1 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 db2e073c8ae..68bd422185a 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 0795079c6b1..28f7b05465f 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 ca734c36572..b74be659eed 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 fe1f220af6d..51b2677368e 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 ecb81c44503..93126d98301 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 f6981a22204..9f5775d478f 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 8e9696af70f..82e09ab72c7 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 a2e81fb3041..fa67ea1d012 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 6384bf920f3..5182dfccf5c 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 dd5cfa64a78..5865d3c36dd 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 36465f5d5f1..afd2a146aff 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 78fa5dbd870..a098178429a 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 3eed66402d1..e99429d3b22 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 @@ private PlaybackInfo getResetPlaybackInfo( 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 4fe8da92c2f..6ab0838e26d 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 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti 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 @@ private void resetInternal( } } - queue.clear(/* keepFrontPeriodUid= */ !resetPosition); + queue.clear(/* keepFrontPeriodUid= */ !resetState); setIsLoading(false); if (resetState) { queue.setTimeline(Timeline.EMPTY); @@ -896,7 +896,7 @@ private void resetInternal( } 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 669f41ca134..e9b99acd778 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 @@ public PlaybackInfo( * * @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 d5b0b2c6677..f924dfb34ce 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 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.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 void run(SimpleExoPlayer player) { 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 bfdf9e733e1..03b45fc945c 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 d97a53bc4d7..ec6e94e0427 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 View[] getAdOverlayViews() { // Internal methods. private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1491,9 +1494,6 @@ public void onLayoutChange( @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 03b45fc945c..4ffd8c4e5de 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 de411089ab1..7177cde12e5 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 void release() { 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 00000000000..af0844defbf --- /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 4ffd8c4e5de..06cfde8d6c4 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 f076eae32cc..847c87b0773 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 @@ default List getStreamKeys(List trackSelections) { * 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 40d4e468bdb..f7edf62182a 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 @@ DashChunkSource createDashChunkSource( * @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 b34b677d45f..5daa1a8fd59 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 @@ private void selectNewStreams( 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 2de81a2535e..bcf0a1766ad 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 DashChunkSource createDashChunkSource( 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 DashChunkSource createDashChunkSource( protected final RepresentationHolder[] representationHolders; + private TrackSelection trackSelection; private DashManifest manifest; private int periodIndex; private IOException fatalError; @@ -222,6 +222,11 @@ public void updateManifest(DashManifest newManifest, int newPeriodIndex) { } } + @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 261c9b531cd..ee5a5f0809b 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 @@ public TrackGroup getTrackGroup() { } /** - * 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 c8c1b8f566b..ff725ec6f7f 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 @@ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStre 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 59e18195e20..22dfb04f130 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 SsChunkSource createChunkSource( 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 void updateManifest(SsManifest newManifest) { 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 b763a484b80..111393140e5 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 @@ SsChunkSource createChunkSource( * @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 286ec82ed64..d103358d370 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 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF 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 00000000000..07055905a69 --- /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 00000000000..ec0cf8df05b --- /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 00000000000..a9fedb19cb6 --- /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 00000000000..ef6da7e3c67 --- /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 249271dc612..e37f192c97f 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 ImaAdsLoader(Context context, Uri adTagUri) { * @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 00000000000..9a382eb18f7 --- /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 00000000000..a66904b5053 --- /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 00000000000..79c544fc0f3 --- /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 00000000000..54eb4d5967c --- /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 00000000000..0848937fdcc --- /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 00000000000..cb16630bd30 --- /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 00000000000..7e0e2442313 --- /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 e408035e982..3a194e091a6 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 PlayerControlView(Context context) { } 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 void setExtraAdGroupMarkers( /** * 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 void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) /** * Sets the {@link PlaybackPreparer}. * - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback + * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { this.playbackPreparer = playbackPreparer; 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 ec6e94e0427..0d66922cabb 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 static void switchTargetView( } /** 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 void setControllerHideDuringAds(boolean controllerHideDuringAds) { /** * 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 void setControllerVisibilityListener(PlayerControlView.VisibilityListener /** * 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 void setExtraAdGroupMarkers( * @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 void setAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener li * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code * null}. */ + @Nullable public View getVideoSurfaceView() { return surfaceView; } @@ -1047,6 +1053,7 @@ public FrameLayout getOverlayFrameLayout() { * @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 00000000000..85903f46593 --- /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 00000000000..bbbffc7a448 --- /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 9ed5cb7e361..daa6124df65 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 @@ private 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 392d4d91790..7ff7912729a 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.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 FragmentedMp4Extractor( 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 @@ private void maybeInitExtraTracks() { } } - /** - * 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 @@ private void onEmsgLeafAtomRead(ParsableByteArray atom) { 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 33d79917ebc..87d0491a7bd 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 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 7ff7912729a..5eaa5d5d319 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 @@ private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime // 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 @@ private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime // 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 87d0491a7bd..a49bf956b32 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 Metadata decode(MetadataInputBuffer inputBuffer) { // 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 8bf142f397c..1419f8198ca 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 @@ protected Pair parseEvent( 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 be085ae30ba..cb1788f2fca 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 void setErrorMessageProvider( * * @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 void setQueueNavigator(QueueNavigator queueNavigator) { * * @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 void invalidateMediaSessionMetadata() { 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 void invalidateMediaSessionMetadata() { */ 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 void invalidateMediaSessionPlaybackState() { 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 void invalidateMediaSessionPlaybackState() { 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 MediaMetadataCompat getMetadata(Player player) { 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 MediaMetadataCompat getMetadata(Player player) { } } } - 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 d076404bb4b..d72f6ffddcc 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 void onAddQueueItem(Player player, MediaDescriptionCompat description) { @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 a0531854357..c69908c7465 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 @@ public DownloadRequest[] load() throws IOException { 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 @@ private static DownloadRequest readDownloadRequest(DataInputStream input) throws 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 @@ private static DownloadRequest readDownloadRequest(DataInputStream input) throws && (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 baf47772ab5..9ecce6e1503 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 static void upgradeAndDelete( 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 6952413129c..54360f8f6b8 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 static DownloadHelper forSmoothStreaming( */ 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 @@ private TrackSelectorResult runTrackSelection(int periodIndex) { 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 ec5ff81d97e..c3cf0bdc24f 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 @@ private void setStopReason(@Nullable String id, int stopReason) { 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 @@ private void setMinRetryCount(int minRetryCount) { } 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 @@ private void addDownload(DownloadRequest request, int stopReason) { } 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 @@ private void syncTasks() { 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 @@ private void onTaskStopped(Task task) { 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 @@ private static class Task extends Thread implements Downloader.ProgressListener 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 void run() { } 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 void onProgress(long contentLength, long bytesDownloaded, float percentDo 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 107cedd7285..db10517b67e 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 void onCreate() { 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 int onStartCommand(Intent intent, int flags, int startId) { // 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 int onStartCommand(Intent intent, int flags, int startId) { } 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 1643812ece2..5326220452a 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 final void download(@Nullable ProgressListener progressListener) 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 | 31 ++++++++++--------- 7 files changed, 57 insertions(+), 35 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 d1660c3306f..1dc25576a08 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 @@ * @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 c5d80aa32bf..39d1ee4094f 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 FfmpegAudioRenderer( } @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 int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { } @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 376d0fd75e9..d833c47d140 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 LibflacAudioRenderer( } @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 @@ protected int supportsFormatInternal(DrmSessionManager drmSessio } @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 b8b95989892..2e9638c447b 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 LibopusAudioRenderer( } @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 @@ protected int supportsFormatInternal(DrmSessionManager drmSessio } @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 dbce33b923c..d93036113c5 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 @@ 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 @@ * 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 @@ private native long opusInit(int sampleRate, int channelCount, int numStreams, i 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 b17fa75181f..e4691db7c01 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 final int supportsFormat(Format format) { * @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 @@ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, * @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 950061e9bc3..6769f5049b4 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.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; - } - - @Override - protected SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException { - return new FakeDecoder(); - } - }; + audioRenderer = + new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { + @Override + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { + return FORMAT_HANDLED; + } + + @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 cc1742bb31d..e20daeebad0 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 ParametersBuilder setPreferredTextLanguage(@Nullable String preferredText return this; } + @Override + public ParametersBuilder setPreferredRoleFlags(int preferredRoleFlags) { + super.setPreferredRoleFlags(preferredRoleFlags); + return this; + } + @Override public ParametersBuilder setSelectUndeterminedTextLanguage( boolean selectUndeterminedTextLanguage) { @@ -642,6 +648,7 @@ public Parameters build() { allowAudioMixedSampleRateAdaptiveness, // Text preferredTextLanguage, + preferredRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags, // General @@ -837,6 +844,7 @@ private Parameters() { /* allowAudioMixedSampleRateAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, + TrackSelectionParameters.DEFAULT.preferredRoleFlags, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, TrackSelectionParameters.DEFAULT.disabledTextTrackSelectionFlags, // General @@ -869,6 +877,7 @@ private Parameters() { boolean allowAudioMixedSampleRateAdaptiveness, // Text @Nullable String preferredTextLanguage, + int preferredRoleFlags, boolean selectUndeterminedTextLanguage, @C.SelectionFlags int disabledTextTrackSelectionFlags, // General @@ -882,6 +891,7 @@ private Parameters() { super( preferredAudioLanguage, preferredTextLanguage, + preferredRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); // Video @@ -2590,6 +2600,7 @@ protected static final class TextTrackScore implements Comparable 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); + (preferredLanguageScore > 0 || (parameters.preferredTextLanguage == null && selectedAudioLanguageScore > 0 && preferredRoleFlagsScore > 0)) || isDefault || (isForced && selectedAudioLanguageScore > 0); } /** @@ -2634,6 +2646,9 @@ public int compareTo(TextTrackScore other) { 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 81af551b685..2edbbc8bd40 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 static class Builder { @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 Builder() { /* package */ Builder(TrackSelectionParameters initialValues) { preferredAudioLanguage = initialValues.preferredAudioLanguage; preferredTextLanguage = initialValues.preferredTextLanguage; + preferredRoleFlags = initialValues.preferredRoleFlags; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; } @@ -74,6 +76,17 @@ public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) 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 TrackSelectionParameters build() { preferredAudioLanguage, // Text preferredTextLanguage, + preferredRoleFlags, selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); } @@ -121,6 +135,11 @@ public TrackSelectionParameters build() { * 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 TrackSelectionParameters build() { /* preferredAudioLanguage= */ null, // Text /* preferredTextLanguage= */ null, + /* preferredRoleFlags= */ 0, /* selectUndeterminedTextLanguage= */ false, /* disabledTextTrackSelectionFlags= */ 0); } @@ -145,12 +165,14 @@ public TrackSelectionParameters build() { /* 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 TrackSelectionParameters build() { /* 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 boolean equals(@Nullable Object obj) { 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 int hashCode() { 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 int describeContents() { 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 e20daeebad0..d07682f3b9c 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 @@ private Parameters() { 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 847c87b0773..3f306c0c8a7 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 @@ default List getStreamKeys(List trackSelections) { * 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 06cfde8d6c4..7c934c478c7 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 cc1742bb31d..77a7acc9e4e 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 static final class ParametersBuilder extends TrackSelectionParameters.Bui private boolean exceedAudioConstraintsIfNecessary; private boolean allowAudioMixedMimeTypeAdaptiveness; private boolean allowAudioMixedSampleRateAdaptiveness; + private boolean allowAudioMixedChannelCountAdaptiveness; // General private boolean forceLowestBitrate; private boolean forceHighestSupportedBitrate; @@ -227,6 +228,8 @@ private ParametersBuilder(Parameters initialValues) { exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; + allowAudioMixedChannelCountAdaptiveness = + initialValues.allowAudioMixedChannelCountAdaptiveness; // General forceLowestBitrate = initialValues.forceLowestBitrate; forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate; @@ -424,6 +427,17 @@ public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( 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 Parameters build() { exceedAudioConstraintsIfNecessary, allowAudioMixedMimeTypeAdaptiveness, allowAudioMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness, // Text preferredTextLanguage, selectUndeterminedTextLanguage, @@ -775,6 +790,12 @@ public static Parameters getDefaults(Context context) { * 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 @@ private Parameters() { /* exceedAudioConstraintsIfNecessary= */ true, /* allowAudioMixedMimeTypeAdaptiveness= */ false, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, @@ -867,6 +889,7 @@ private Parameters() { boolean exceedAudioConstraintsIfNecessary, boolean allowAudioMixedMimeTypeAdaptiveness, boolean allowAudioMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness, // Text @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -901,6 +924,7 @@ private Parameters() { this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; // General this.forceLowestBitrate = forceLowestBitrate; this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; @@ -934,6 +958,7 @@ private Parameters() { 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 boolean equals(@Nullable Object obj) { && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness + && allowAudioMixedChannelCountAdaptiveness + == other.allowAudioMixedChannelCountAdaptiveness // General && forceLowestBitrate == other.forceLowestBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate @@ -1045,6 +1072,7 @@ public int hashCode() { 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 void writeToParcel(Parcel dest, int flags) { 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 @@ protected Pair selectAudioTrack( 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 @@ private static int[] getAdaptiveAudioTracks( 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 @@ private static int[] getAdaptiveAudioTracks( configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness); + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness); if (configurationCount > selectedConfigurationTrackCount) { selectedConfiguration = configuration; selectedConfigurationTrackCount = configurationCount; @@ -2044,7 +2076,8 @@ private static int[] getAdaptiveAudioTracks( selectedConfiguration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { adaptiveIndices[index++] = i; } } @@ -2059,7 +2092,8 @@ private static int getAdaptiveAudioTrackCount( 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 @@ private static int getAdaptiveAudioTrackCount( configuration, maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { count++; } } @@ -2081,11 +2116,13 @@ private static boolean isSupportedAdaptiveAudioTrack( 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 4622dc17347..0374f88bae9 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 void testParametersParcelable() { /* 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 77a7acc9e4e..8e1284f7ef4 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 ParametersBuilder clearVideoSizeConstraints() { } /** - * 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 ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) } /** - * 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 ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) { } /** - * 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 ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { } /** - * 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 ParametersBuilder setExceedVideoConstraintsIfNecessary( } /** - * 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 ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness( } /** - * 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 ParametersBuilder setAllowVideoNonSeamlessAdaptiveness( * 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 ParametersBuilder clearViewportSizeConstraints() { } /** - * 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 ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAud } /** - * 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 ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { } /** - * 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 ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) { } /** - * 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 ParametersBuilder setExceedAudioConstraintsIfNecessary( } /** - * 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 ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness( } /** - * 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 ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( } /** - * 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 ParametersBuilder setDisabledTextTrackSelectionFlags( // 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 ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { } /** - * 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 ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamles } /** - * 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 ParametersBuilder setExceedRendererCapabilitiesIfNecessary( } /** - * 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 ParametersBuilder setExceedRendererCapabilitiesIfNecessary( * * @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 ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) * * @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 final ParametersBuilder setRendererDisabled(int rendererIndex, boolean di * @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 final ParametersBuilder setSelectionOverride( * * @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 final ParametersBuilder clearSelectionOverride( * 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 final ParametersBuilder clearSelectionOverrides(int rendererIndex) { 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 static Parameters getDefaults(Context context) { // 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 static Parameters getDefaults(Context context) { */ 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 static Parameters getDefaults(Context context) { */ 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 static Parameters getDefaults(Context context) { */ 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 static Parameters getDefaults(Context context) { */ 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 static Parameters getDefaults(Context context) { */ 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 81af551b685..c406f262d1f 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 Builder() { } /** - * 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 Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage } /** - * 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 Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) } /** - * 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 Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextL } /** - * 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 a49bf956b32..340b662e976 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 Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); - ParsableByteArray emsgData = new ParsableByteArray(data, size); + return new Metadata(decode(new ParsableByteArray(data, size))); + } + + public EventMessage decode(ParsableByteArray emsgData) { String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); long timescale = emsgData.readUnsignedInt(); @@ -50,8 +53,9 @@ public Metadata decode(MetadataInputBuffer inputBuffer) { 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 9f5775d478f..c34ed8c907d 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 5daa1a8fd59..21fd43da214 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.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 @@ public List getStreamKeys(List trackSelections) { } @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 576491b4645..890a272c5ef 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 Factory( * @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 int[] getSupportedTypes() { 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 DashMediaSource( 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 DashMediaSource( 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 DashMediaSource( 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 DashMediaSource( 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 DashMediaSource( } 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 6a6e08ce1d7..c9433b9e41a 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 static DashManifest loadManifest(DataSource dataSource, Uri uri) * @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 static DashManifest loadManifest(DataSource dataSource, Uri uri) * @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 static DashManifest loadManifest(DataSource dataSource, Uri uri) * @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 static DashManifest loadManifest(DataSource dataSource, Uri uri) * @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 @@ private static ChunkExtractorWrapper newWrappedExtractor(int trackType, Format f 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 @@ private static ChunkExtractorWrapper newWrappedExtractor(int trackType, Format f } 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 bcf0a1766ad..cd39c9538aa 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 static final class Factory implements DashChunkSource.Factory { 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 @@ protected static final class RepresentationHolder { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { this( periodDurationUs, representation, @@ -787,7 +787,7 @@ private static boolean mimeTypeIsRawText(String mimeType) { 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 0c3f641cbe1..2d8909f8b44 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 DashManifest( 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 DashManifest( 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 1419f8198ca..8affcb27ce5 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.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 @@ protected DashManifest buildMediaPresentationDescription( 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 @@ protected Period buildPeriod(String id, long startMs, List adapta // 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 @@ protected int getContentType(Format format) { * @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 @@ protected void parseAdaptationSetChild(XmlPullParser xpp) 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 @@ protected RepresentationInfo parseRepresentation( } 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 @@ protected Format buildFormat( protected Representation buildRepresentation( RepresentationInfo representationInfo, - String extraDrmSchemeType, + @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; @@ -675,7 +678,8 @@ protected Representation buildRepresentation( // 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 @@ protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, lon 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 @@ protected SegmentList buildSegmentList( 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 @@ protected SegmentTemplate buildSegmentTemplate( 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 @@ protected SegmentTimelineElement buildSegmentTimelineElement(long elapsedTime, l 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 @@ protected int parseRoleFlagsFromAccessibilityDescriptors( } @C.RoleFlags - protected int parseDashRoleSchemeValue(String value) { + protected int parseDashRoleSchemeValue(@Nullable String value) { if (value == null) { return 0; } @@ -1159,7 +1164,7 @@ protected int parseDashRoleSchemeValue(String value) { } @C.RoleFlags - protected int parseTvaAudioPurposeCsValue(String value) { + protected int parseTvaAudioPurposeCsValue(@Nullable String value) { if (value == null) { return 0; } @@ -1230,7 +1235,9 @@ private static void filterRedundantIncompleteSchemeDatas(ArrayList s * @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 @@ private static String getSampleMimeType(String containerMimeType, String codecs) * @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 @@ private static boolean mimeTypeIsRawText(String mimeType) { } /** - * 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 @@ protected static final class RepresentationInfo { public final Format format; public final String baseUrl; public final SegmentBase segmentBase; - public final String drmSchemeType; + @Nullable public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public final long revisionId; - public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams, long revisionId) { + public RepresentationInfo( + Format format, + String baseUrl, + SegmentBase segmentBase, + @Nullable String drmSchemeType, + ArrayList drmSchemeDatas, + ArrayList inbandEventStreams, + long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 493a8da09c1..d68690d3630 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 @@ */ 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 boolean equals(@Nullable Object obj) { @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 62934d74338..ac264bd2b10 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 @@ /** 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 9ac1257ee2f..bcd783f0cba 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 String resolveUriString(String baseUri) { *

          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 0884bcc65c8..80ad15cd8f5 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 static Representation newInstance( 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 static Representation newInstance( 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 @@ private 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 @@ private 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 RangedUri getInitializationUri() { * 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 static class SingleSegmentRepresentation extends 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 static SingleSegmentRepresentation newInstance( 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 SingleSegmentRepresentation( 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 SingleSegmentRepresentation( } @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 MultiSegmentRepresentation( 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 DashSegmentIndex getIndex() { } @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 ba4faafd95e..a31e0329af2 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 @@ */ 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 SegmentBase(RangedUri initialization, long timescale, long presentationTi * @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 static class SingleSegmentBase extends 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 static class MultiSegmentBase extends 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 static class MultiSegmentBase extends 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 boolean isExplicit() { */ 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 SegmentList( 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 boolean isExplicit() { */ 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 SegmentTemplate( 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 SegmentTemplate( } @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 00000000000..b7c267727c5 --- /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 00000000000..4eb0d8436d9 --- /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 00000000000..f51ea4369e3 --- /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 82e09ab72c7..8301820e793 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 022d62cbfc2..fe70298dc80 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 @@ public final int read(byte[] buffer, int offset, int readLength) throws IOExcept } @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 9fde54a705d..6dd4ade5906 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 DefaultHlsExtractorFactory( @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 ee5a5f0809b..c452a29cf92 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 @@ public HlsChunkHolder() { 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 @@ public HlsChunkHolder() { 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 @@ public HlsChunkSource( 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 103d89188fa..927b79899d7 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 Result(Extractor extractor, boolean isPackedAudioExtractor, boolean isReu * @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 e21827557a1..8053958c2bc 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.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 List getStreamKeys(List trackSelections) { } @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 877b6d486ea..f2db9541ebe 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 Factory(HlsDataSourceFactory hlsDataSourceFactory) { * @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 665f2e05701..a89e907a373 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 00000000000..2527553824b --- /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 00000000000..55f15f5e7af --- /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 a4fd28009f9..e7a072839e0 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 HlsMasterPlaylist getMasterPlaylist() { } @Override + @Nullable public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); if (snapshot != null && isForPlayback) { @@ -448,7 +449,7 @@ private final class MediaPlaylistBundle 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 MediaPlaylistBundle(Uri playlistUrl) { 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 0e86df8c2fd..1660324a34f 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 Rendition(@Nullable Uri url, Format format, String groupId, String name) * 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 HlsMasterPlaylist( 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 4411c9865ef..58f500cf947 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 Segment( 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 Segment( } @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 00000000000..61f9d77e720 --- /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 340b662e976..f592a6eee71 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 Metadata decode(MetadataInputBuffer inputBuffer) { 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 dd33d591a72..4fa3f71b32d 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 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 d870afac3a5..88a61d0bce7 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 ca8303d3e27..56830035ccf 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 @@ @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 b6663ac3d75..5e9d8d0897b 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 LibvpxVideoRenderer(long allowedJoiningTimeMs) { */ public LibvpxVideoRenderer( long allowedJoiningTimeMs, - Handler eventHandler, - VideoRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { this( allowedJoiningTimeMs, @@ -206,10 +206,10 @@ public LibvpxVideoRenderer( */ 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 LibvpxVideoRenderer( */ 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 93a4a2fc1fb..0efd4bd0ea9 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 @@ 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 @@ public VpxDecoder( int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - ExoMediaCrypto exoMediaCrypto, + @Nullable ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads) @@ -170,9 +170,19 @@ private native long vpxInit( 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 8c765952e7a..4e983cccc7a 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 @@ 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 00000000000..b8725607a50 --- /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 fa67ea1d012..d85ecbb1a3d 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 d103358d370..b3d950959aa 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 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 @@ public TrackGroupArray 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) { 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 3c0593200ee..9ddc7aa0f08 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 Factory( * @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 int[] getSupportedTypes() { 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 SsMediaSource( 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 SsMediaSource( 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 SsMediaSource( 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 SsMediaSource( 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 SsMediaSource( } 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 cfb772a86bb..b91bfc8f675 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 static class StreamElement { 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 static class StreamElement { 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 StreamElement(String baseUri, String chunkTemplate, int type, String subT 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 Uri buildRequestUri(int track, int chunkIndex) { 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 SsManifest( long dvrWindowLength, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this( majorVersion, @@ -273,7 +297,7 @@ private SsManifest( 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 39e22f29820..03e9e91e227 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.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 @@ private abstract static class ElementParser { 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 @@ private ElementParser newChildParser(ElementParser parent, String name, String b * 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 @@ private static class SmoothStreamingMediaParser extends ElementParser { 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 00000000000..b594ddc2bcc --- /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 00000000000..f7c74f1a1eb --- /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 00000000000..23e85850c60 --- /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 b1c2c636c7b..9510b8442ec 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 93126d98301..8e643836388 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 1f7a8d0ee74..ab3cc5fccd3 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 bd7a70eba03..7678710f186 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.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 @@ */ @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 9510b8442ec..b1c2c636c7b 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 8e643836388..93126d98301 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 ab3cc5fccd3..1f7a8d0ee74 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 7678710f186..bd7a70eba03 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.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 @@ */ @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 5e9d8d0897b..b000ea1b6b5 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 @@ private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackExcepti 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 a7754816338..0fc0a85104e 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx // 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 38889e1a21a..3f6d58f7775 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 68a7494a3fe..4af8f94c583 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 f7cc707fb41..9c49ba94e15 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 15952b1860e..2b5a6010a97 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 c67de276979..dfac2e1c26b 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 0ef9f281c90..41d6aaf6283 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 28f7b05465f..7b621a8df9c 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 b74be659eed..74ef70fbf06 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 51b2677368e..3b8271869be 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 93126d98301..fda2f079de5 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 c34ed8c907d..c64da2b86d8 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 8301820e793..0f685c11301 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 d85ecbb1a3d..b16157f49bd 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 5182dfccf5c..5b3123e302d 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 afd2a146aff..b5e68187bed 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 a098178429a..00000000000 --- 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 057caad8673..00000000000 --- 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 41d6aaf6283..340e9832be1 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 8affcb27ce5..2d503a8763c 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 @@ protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long 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 @@ protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long } 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 @@ protected Period buildPeriod(String id, long startMs, List adapta // 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 @@ protected AdaptationSet parseAdaptationSet( roleDescriptors, accessibilityDescriptors, supplementalProperties, - segmentBase); + segmentBase, + periodDurationMs); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -338,7 +339,8 @@ protected AdaptationSet parseAdaptationSet( 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 @@ protected RepresentationInfo parseRepresentation( 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 @@ protected RepresentationInfo parseRepresentation( } 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 @@ protected SegmentList parseSegmentList(XmlPullParser xpp, @Nullable SegmentList 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 @@ protected SegmentList buildSegmentList( 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 @@ protected SegmentTemplate parseSegmentTemplate( 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 @@ protected EventMessage buildEvent( 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 @@ protected List parseSegmentTimeline(XmlPullParser xpp) 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 7c934c478c7..c1a9b0f4c7a 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 4145d9a1c7f..9f76ca544f0 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 Builder(Context context) { initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context)); slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT; clock = Clock.DEFAULT; + resetOnNetworkTypeChange = true; } /** @@ -168,14 +169,12 @@ public Builder setClock(Clock clock) { } /** - * 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 b000ea1b6b5..696221cae82 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 @@ 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 @@ * 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 LibvpxVideoRenderer( 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; - } - 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 (VpxDecoderException 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 || 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; - } - } - - @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; + if (format.drmInitData == null + || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)) { + return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; } - formatQueue.clear(); + return super.supportsFormat(format); } @Override - protected void onStarted() { - droppedFrames = 0; - droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + 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 - protected void onStopped() { - joiningDeadlineMs = C.TIME_UNSET; - maybeNotifyDroppedFrames(); + protected VideoDecoderOutputBuffer dequeueOutputBuffer() throws VpxDecoderException { + outputBuffer = decoder.dequeueOutputBuffer(); + return outputBuffer; } @Override - protected void onDisabled() { - format = null; - waitingForKeys = false; - clearReportedVideoSize(); - clearRenderedFirstFrame(); - try { - setSourceDrmSession(null); - releaseDecoder(); - } finally { - eventDispatcher.disabled(decoderCounters); + protected void renderOutputBuffer(long presentationTimeUs, Format outputFormat) + throws VpxDecoderException { + if (frameMetadataListener != null) { + frameMetadataListener.onVideoFrameAboutToBeRendered( + presentationTimeUs, System.nanoTime(), outputFormat); } - } - @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); - } - } - - 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 @@ protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecode 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 @@ private void setOutput( 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(); - } - } 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(); - } - } - - 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); + onOutputSurfaceRemoved(); } - renderOutputBuffer(outputBuffer); - return true; + } else if (hasOutputSurface()) { + onOutputSurfaceReset(surface); } - - 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 00000000000..d66155548d9 --- /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 4ebe97313b1..7077416a022 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 @@ protected abstract void onChildSourceInfoRefreshed( /** * 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 2ab7e613780..7061521b53b 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 @@ protected Surface getSurface() { 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 76742a8691a..360279c11c9 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 af0844defbf..b4b09b20a28 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 boolean initForYuvFrame(int width, int height, int yStride, int uvStride, 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 c78b02aa5b3..5314835d1e6 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.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 @@ protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error 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 @@ protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error } 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 50eb048d98b..890d82a0067 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.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 @@ protected FlacDecoderException decode( 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 d93036113c5..f0e993e3b98 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.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 @@ protected OpusDecoderException decode( // 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 @@ protected OpusDecoderException decode( } } - 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 0efd4bd0ea9..1392e782f84 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.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 @@ protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { @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 1099b14bfcc..f5db0145fe7 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 final void setIndex(int index) { } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -105,6 +106,7 @@ public final void replaceStream(Format[] formats, SampleStream stream, long offs } @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 e901025a079..894736571ca 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 final void setIndex(int index) { } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -113,6 +114,7 @@ public final void replaceStream(Format[] formats, SampleStream stream, long offs } @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 9f52e8d9def..9e44e3741c1 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 @@ void enable(RendererConfiguration configuration, Format[] formats, SampleStream 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 00000000000..2764120d2ae --- /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 4e4964e8177..05c20939ff2 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 @@ private SyncFrameInfo( * @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 static Format parseAc3AnnexFFormat( * @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 74bd5bfe98a..c54e3844a30 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 @@ private SyncFrameInfo( * @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 9c63eb42c62..1b0d629da72 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 AudioAttributes build() { 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 f65dc3fc4e9..7af9d9f0743 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 static boolean isSyncWord(int word) { * @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 6a29f316e18..251901f4f26 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 MediaCodecAudioRenderer( } @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 @@ protected void configureCodec( 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 00000000000..5ae2413d926 --- /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 00000000000..4921e1aeea0 --- /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 7eb1fa1aa11..4552d190c33 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 7fc6fb625ae..c31ae92cfc9 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 DecoderInputBuffer(@BufferReplacementMode int bufferReplacementMode) { * @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 b5650860e9a..b7465f82eb0 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 @@ protected final void setInitialInputBufferSize(int size) { } @Override + @Nullable public final I dequeueInputBuffer() throws E { synchronized (lock) { maybeThrowException(); @@ -108,6 +109,7 @@ public final void queueInputBuffer(I inputBuffer) throws E { } @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 49c7dafbd6b..84cffc11453 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 00000000000..0c4dbde9d34 --- /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 00000000000..d4820dd204b --- /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 974e033b670..ee2c9ad1a39 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 final int supportsFormat(Format format) throws ExoPlaybackException { * @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 @@ protected abstract void configureCodec( 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 cd4c4863ffb..9c42916cada 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 static int maxH264DecodableFrameSize() throws DecoderQueryException { * @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 00000000000..b09404a6f80 --- /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 f592a6eee71..a1196c41c85 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 3d873926bbe..12f65f1cdaf 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 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 85a59c3aebc..c8755f9aee5 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.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 Id3Decoder(@Nullable FramePredicate framePredicate) { @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 00000000000..690f2c40c36 --- /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 00000000000..6273f325c4e --- /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 c3eab689833..3bb7ada7e09 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 int readData( 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 00000000000..45131e644b0 --- /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 74e50dfd926..f2259e8f1a3 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 void write(int b) throws IOException { } @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 8f1a6544ca8..5b85b26c3f5 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 @@ 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 ee469a5b2ad..1fbea2ed7eb 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 @@ 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 @@ public Message obtainMessage(int what) { } @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 @@ public Message obtainMessage(int what, int arg1, int arg2) { } @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 @@ public void removeMessages(int what) { } @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 071ebf20842..60f4fa17dd8 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 @@ private 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 e700fc67511..6db5ddef39f 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 static boolean areEqual(@Nullable Object o1, @Nullable Object o2) { /** * 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 static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) { * @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 00000000000..76899fc4525 --- /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 7061521b53b..de77e8318d1 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 MediaCodecVideoRenderer( } @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 @@ protected void configureCodec( 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 d66155548d9..2dd43fb6d64 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 @@ protected void updateDroppedBufferCounters(int droppedBufferCount) { /** * 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 00000000000..3c2cd217e0c --- /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 03822be17c7..7d76f43d04c 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 00000000000..2dce6889d8b --- /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 f06a709960d..6e67be6ec5a 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 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 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 696221cae82..4c20cd7a82d 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 int supportsFormat(Format format) { } @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 4c20cd7a82d..fb68a36949f 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 LibvpxVideoRenderer( } @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 2dd43fb6d64..3d18242466b 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.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 @@ protected SimpleDecoderVideoRenderer( // 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 @@ protected void updateDroppedBufferCounters(int droppedBufferCount) { } } + /** + * 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 fb68a36949f..aea422176ee 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 b2bd20f0fe6..37539845dc9 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 static Format createVideoContainerFormat( containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, width, height, @@ -216,6 +217,7 @@ public static Format createVideoContainerFormat( @Nullable String containerMimeType, String sampleMimeType, String codecs, + @Nullable Metadata metadata, int bitrate, int width, int height, @@ -230,7 +232,7 @@ public static Format createVideoContainerFormat( roleFlags, bitrate, codecs, - /* metadata= */ null, + metadata, containerMimeType, sampleMimeType, /* maxInputSize= */ NO_VALUE, @@ -363,8 +365,8 @@ public static Format createVideoSampleFormat( // 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 static Format createAudioContainerFormat( containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, channelCount, sampleRate, @@ -399,6 +402,7 @@ public static Format createAudioContainerFormat( @Nullable String containerMimeType, @Nullable String sampleMimeType, @Nullable String codecs, + @Nullable Metadata metadata, int bitrate, int channelCount, int sampleRate, @@ -413,7 +417,7 @@ public static Format createAudioContainerFormat( 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 8affcb27ce5..9f6cce672e6 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 @@ protected Format buildFormat( containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, bitrate, width, height, @@ -610,6 +611,7 @@ protected Format buildFormat( 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 a53b1ff80da..6e769b72e1f 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 @@ private static Representation newRepresentations(DrmInitData drmInitData) { 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 8053958c2bc..f4f91cf1b4f 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 @@ private static Format deriveVideoFormat(Format variantFormat) { variantFormat.containerMimeType, sampleMimeType, codecs, + /* metadata= */ null, variantFormat.bitrate, variantFormat.width, variantFormat.height, @@ -829,6 +830,7 @@ private static Format deriveAudioFormat( 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 42b27f259f0..030520f8cb8 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 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, /* sampleMimeType= */ null, codecs, + /* metadata= */ null, bitrate, width, height, @@ -422,6 +423,7 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, sampleMimeType, codecs, + /* metadata= */ null, /* bitrate= */ Format.NO_VALUE, width, height, @@ -451,6 +453,7 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, sampleMimeType, codecs, + /* metadata= */ null, /* bitrate= */ Format.NO_VALUE, channelCount, /* sampleRate= */ Format.NO_VALUE, 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 93b8be3346e..847e46591d8 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 @@ -128,6 +128,7 @@ private static Variant createMuxedVideoAudioVariant(int bitrate) { /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, /* sampleMimeType= */ null, /* codecs= */ "avc1.100.41,mp4a.40.2", + /* metadata= */ null, bitrate, /* width= */ Format.NO_VALUE, /* height= */ Format.NO_VALUE, @@ -145,6 +146,7 @@ private static Variant createAudioOnlyVariant(int bitrate) { /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, /* sampleMimeType= */ null, /* codecs= */ "mp4a.40.2", + /* metadata= */ null, bitrate, /* width= */ Format.NO_VALUE, /* height= */ Format.NO_VALUE, @@ -177,6 +179,7 @@ private static Format createAudioFormat(String language) { /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, MimeTypes.getMediaMimeType("mp4a.40.2"), /* codecs= */ "mp4a.40.2", + /* metadata= */ null, /* bitrate= */ Format.NO_VALUE, /* channelCount= */ Format.NO_VALUE, /* sampleRate= */ Format.NO_VALUE, 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 03e9e91e227..c08a867f37b 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 @@ -681,6 +681,7 @@ public void parseStartTag(XmlPullParser parser) throws ParserException { MimeTypes.VIDEO_MP4, sampleMimeType, /* codecs= */ null, + /* metadata= */ null, bitrate, width, height, @@ -706,6 +707,7 @@ public void parseStartTag(XmlPullParser parser) throws ParserException { 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 c1a9b0f4c7a..2fe1cf8a546 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 daa6124df65..cd862e503f4 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 @@ private 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 972f651a41a..1244b96d946 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 @@ protected int calculateTargetBufferSize( 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 @@ private void reset(boolean resetAllocator) { } } + 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 6db5ddef39f..35a36c3cd56 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 static String getStringForTime(StringBuilder builder, Formatter formatter : 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 0e129ca7bb6..8337911c0d2 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 int hashCode() { @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 7b4f4c08360..dbc1114bd51 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 int hashCode() { 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 2fe1cf8a546..bf89c837242 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 8b75eb0c748..b1a12f6bc9d 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.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 @@ public PlayerManager( 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 059f26b3744..15cc9b6469f 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.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 @@ private void initializePlayer() { 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 8f2c891e3a7..3caf7f0c165 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 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 @@ public PlayerManager(Context context) { 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 1e231dd45e9..d3c32ac9572 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 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 @@ private void initializePlayer() { 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 5b68f1e352c..dd9ce38d35c 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 78035f4d873..b4b0dae0024 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 c10d6fdb273..bf96442f61c 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.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 TestPlaybackRunnable(Uri uri, Context context) { 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 95c68072757..af44e84b04c 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 382ee38e063..b3d5b525d5d 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.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 TestPlaybackRunnable(Uri uri, Context context) { 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 be75eae3596..34230db2ec0 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 9be1d9c0e52..d4e07952937 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.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 TestPlaybackRunnable(Uri uri, Context context) { 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 490d9613962..a97b1e0d5a6 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 DefaultRenderersFactory(Context context) { /** * @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 DefaultRenderersFactory( /** * @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 DefaultRenderersFactory( /** * @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 ee29af9c99c..27c3a630f8e 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.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 @@ */ 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 9168f1bd765..82bc94dab8b 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.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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static ExoPlayer newInstance( 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 static ExoPlayer newInstance( 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 e99429d3b22..cacdaec02ed 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 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 8913fbdaba1..749cf79e781 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.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.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 @@ /** * 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 c5d22c15cb9..f4577010310 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 @@ 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 Factory() { /** * @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 Factory( /** * @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 Factory( /** * @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 8e1284f7ef4..b43701f1bc3 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.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 DefaultTrackSelector() { /** * @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 9f76ca544f0..67dd9a7a551 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 @@ private static int[] getCountryGroupIndices(String countryCode) { } } + /** + * 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 e452e391d58..e2fc0798876 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.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 @@ protected SimpleExoPlayer buildExoPlayer( 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 3ebd47b7a68..214c51c00c3 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.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 @@ protected SimpleExoPlayer buildExoPlayer( 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 27c3a630f8e..991be9b08b8 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 @@ final class Builder { *

        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 f4f91cf1b4f..ad2a3ad2650 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.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 @@ private static Format deriveVideoFormat(Format variantFormat) { variantFormat.containerMimeType, sampleMimeType, codecs, - /* metadata= */ null, + variantFormat.metadata, variantFormat.bitrate, variantFormat.width, variantFormat.height, @@ -800,6 +801,7 @@ private static Format deriveVideoFormat(Format variantFormat) { 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 @@ private static Format deriveAudioFormat( 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 @@ private static Format deriveAudioFormat( 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 @@ private static Format deriveAudioFormat( 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 f4577010310..fc3783d56b9 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 static class Factory implements TrackSelection.Factory { 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 Factory( 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 final void experimental_setTrackBitrateEstimator( AdaptiveTrackSelection adaptiveSelection = createAdaptiveTrackSelection( definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); - adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); adaptiveSelections.add(adaptiveSelection); selections[i] = adaptiveSelection; } @@ -312,11 +296,7 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection( 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 @@ private AdaptiveTrackSelection( 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 void updateSelectedTrack( 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 int evaluateQueueSize(long playbackPositionUs, List 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 @@ protected long getMinDurationToRetainAfterDiscardUs() { * * @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 1cd6c09bfee..00000000000 --- 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 71afd87b0fd..0f2748b1ac8 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 static DefaultTrackSelector.Parameters updateParametersWithOverride( } 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 25f7e4ea73c..00000000000 --- 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 456f7f7107b..af935048e8a 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.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 void testUpdateSelectedTrackSwitchDownIfNotBufferedEnough() { 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 963e90f139e..00000000000 --- 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 d40149baaea..00000000000 --- 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 02d0e372e8d..8b05b270463 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 boolean isReady() { } @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 int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 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 void maybeThrowError() throws IOException { 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 bf89c837242..d166fb41c6d 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 82bc94dab8b..ae5071717d6 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 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 static SimpleExoPlayer newSimpleInstance( 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 749cf79e781..9606cc0d48d 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 static final class Builder { 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 static final class Builder { *

        • {@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 Builder(Context context, RenderersFactory renderersFactory) { renderersFactory, new DefaultTrackSelector(context), new DefaultLoadControl(), - DrmSessionManager.getDummyDrmSessionManager(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), new AnalyticsCollector(Clock.DEFAULT), @@ -160,7 +157,6 @@ public Builder(Context context, RenderersFactory renderersFactory) { * 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 Builder( RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, - DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, Looper looper, AnalyticsCollector analyticsCollector, @@ -180,7 +175,6 @@ public Builder( 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 Builder setLoadControl(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. * @@ -294,7 +275,6 @@ public SimpleExoPlayer build() { renderersFactory, trackSelector, loadControl, - drmSessionManager, bandwidthMeter, analyticsCollector, clock, @@ -354,7 +334,11 @@ public SimpleExoPlayer build() { * 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 @@ protected SimpleExoPlayer( * 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 @@ protected SimpleExoPlayer( 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 @@ protected SimpleExoPlayer( * 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 e2fc0798876..0d966c90807 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 @@ protected DefaultTrackSelector buildTrackSelector(HostActivity host) { } @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 @@ protected DefaultDrmSessionManager buildDrmSessionManager( @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 214c51c00c3..5f01d7724b2 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 final void onStart(HostActivity host, Surface surface) { // 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 final void onStart(HostActivity host, Surface surface) { 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 @@ private boolean stopTest() { 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 @@ protected DefaultTrackSelector buildTrackSelector(HostActivity host) { } 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 aea422176ee..dd4077964bd 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 @@ protected int supportsFormatInternal( 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 3d18242466b..ad59d11c4a6 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 @@ protected abstract int supportsFormatInternal( 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 dbc1114bd51..35702da576c 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 @@ 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 @@ */ 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 0fc0a85104e..0dc0dc6096a 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.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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx } 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx } } + /** + * 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 ca1e3901819..c9e9d540933 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 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 @@ */ 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 EventMessage( 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 00000000000..4de8bb76cc8 --- /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 9fc8b09a18c..d23f07a90ba 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 74ef70fbf06..9c709305bf0 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 2d503a8763c..0516ef6cc96 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 @@ protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long } 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 @@ protected AdaptationSet parseAdaptationSet( } 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 @@ protected RepresentationInfo parseRepresentation( } 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 @@ protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, lon 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 @@ protected SegmentList parseSegmentList(XmlPullParser xpp, @Nullable SegmentList 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 @@ protected List parseSegmentTimeline(XmlPullParser xpp,lo 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 ae5071717d6..efe351c70ac 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 static SimpleExoPlayer newSimpleInstance( 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 9606cc0d48d..ba63dee80e6 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 SimpleExoPlayer build() { @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 d23f07a90ba..303672334db 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 b1c2c636c7b..9510b8442ec 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 fda2f079de5..d6aee8a35dc 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 1f7a8d0ee74..ab3cc5fccd3 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 bd7a70eba03..7678710f186 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.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 @@ */ @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 32fa3a6e4bc..8d5731da203 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 @@ * 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 a1196c41c85..bbf7476d25e 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 4de8bb76cc8..26dcefc6110 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 void decodeMetadata() throws Exception { /* 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 void decodeMetadata_handlesWrappedMetadata() throws Exception { /* 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 void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception { /* 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 af4bf3ad704..883ca7420e5 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 @@ private void parseAndDiscardSamples() { } 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 c9e9d540933..7d35a15e311 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 boolean equals(@Nullable Object obj) { @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 7d35a15e311..6e0b0b40f2b 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 EventMessage( @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 26dcefc6110..af6489f7263 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.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 @@ @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 void decodeMetadata_skipsMalformed() throws Exception { } @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 void decodeMetadata_handlesWrappedMetadata() throws Exception { 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 | 185 +++++++++--------- .../android/exoplayer2/MediaPeriodQueue.java | 76 +++---- .../exoplayer2/MediaPeriodQueueTest.java | 8 +- 4 files changed, 130 insertions(+), 141 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d166fb41c6d..3489c34bbc0 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 6ab0838e26d..53c381961e8 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 @@ 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 @@ private void stopRenderers() throws ExoPlaybackException { } 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 @@ private void updatePlaybackPositions() throws ExoPlaybackException { 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 @@ private void updatePlaybackPositions() throws ExoPlaybackException { 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 @@ && shouldTransitionToReadyState(renderersReadyOrEnded)) { 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 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti 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 @@ private long seekToPeriodPosition( } 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 @@ private void disableRenderer(Renderer renderer) throws ExoPlaybackException { } 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 @@ private void reselectTracksInternal() throws ExoPlaybackException { } 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 @@ private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { } 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 @@ private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) { 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 @@ private void maybeThrowSourceInfoRefreshError() throws IOException { 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 @@ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) } } 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 @@ private long getMaxRendererReadPositionUs() { 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 @@ private void updatePeriods() throws ExoPlaybackException, IOException { 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 @@ private void maybeUpdateLoadingPeriod() throws IOException { } } - private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { + private void maybeUpdateReadingPeriod() throws ExoPlaybackException { MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; @@ -1552,7 +1553,6 @@ private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException if (!readingPeriodHolder.getNext().prepared) { // The successor is not prepared yet. - maybeThrowPeriodPrepareError(); return; } @@ -1607,6 +1607,11 @@ private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { 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 @@ private boolean shouldAdvancePlayingPeriod() { if (playingPeriodHolder == null) { return false; } + MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); + if (nextPlayingPeriodHolder == null) { + return false; + } MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - if (playingPeriodHolder == readingPeriodHolder) { + 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 @@ private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackExc 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 @@ private void enableRenderer( } } - 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 0f279ba6d36..e515877d78d 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 @@ public boolean shouldLoadNextMediaPeriod() { } /** - * 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 @@ public boolean shouldLoadNextMediaPeriod() { * @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 @@ public MediaPeriod enqueueNextMediaPeriod( 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 @@ public MediaPeriodHolder getLoadingPeriod() { /** * 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 @@ public MediaPeriodHolder advanceReadingPeriod() { /** * 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 @@ public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) { * 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 @@ public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererRead // 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 @@ private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) { } } } - 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 @@ private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) { } 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 @@ private boolean areDurationsCompatible(long previousDurationUs, long newDuration */ 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 @@ private boolean updateForPlaybackModeChange() { 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 14aa436be3a..3c6c4462ca7 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 @@ private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { private void advance() { enqueueNext(); - advancePlaying(); + if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) { + advancePlaying(); + } } private void advancePlaying() { @@ -382,7 +384,7 @@ private void advanceReading() { } private void enqueueNext() { - mediaPeriodQueue.enqueueNextMediaPeriod( + mediaPeriodQueue.enqueueNextMediaPeriodHolder( rendererCapabilities, trackSelector, allocator, @@ -460,7 +462,7 @@ private void assertNextMediaPeriodInfoIsAd(int adGroupIndex, long contentPositio 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 3489c34bbc0..8415f57c9a1 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 b84f1c1f2b4..58dbec611a6 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 @@ public int getWindowCount() { } @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 bb14ac147b4..1b3e57ceded 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 final int getPreviousWindowIndex() { @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 8d5731da203..57d3d8bf1d3 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 int getWindowCount() { } @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 int getFirstWindowIndex(boolean shuffleModeEnabled) { } /** - * 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 final Window getWindow(int windowIndex, Window window, boolean setTag) { * * @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 final Pair getPeriodPosition( 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 db197643185..4e7b572384d 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 @@ public int getFirstWindowIndex(boolean shuffleModeEnabled) { } @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 81169354de0..703f8bafc0e 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 ClippingTimeline(Timeline timeline, long startUs, long endUs) } @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 45997aced44..38b373b26cd 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 int getFirstWindowIndex(boolean shuffleModeEnabled) { } @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 d9dd83de4f6..35800f56dad 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 int getWindowCount() { } @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 8790b09f075..966f8e4c7ad 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 int getWindowCount() { } @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 25a1440c802..b5167dc1736 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 Period getPeriod(int periodIndex, Period period, boolean setIds) { } @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 701ec3521c5..cb21db8212b 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 void setNullTag_returnsNullTag_butUsesDefaultUid() { /* 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 void setNullTag_returnsNullTag_butUsesDefaultUid() { } @Test - public void setTag_isUsedForWindowTag() { + public void getWindow_setsTag() { Object tag = new Object(); SinglePeriodTimeline timeline = new SinglePeriodTimeline( @@ -108,9 +107,7 @@ public void setTag_isUsedForWindowTag() { /* 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 890a272c5ef..1b163c02e2a 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 int getWindowCount() { } @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 58ee32cdd9f..f0a0c77ff65 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 int getWindowCount() { } @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 1e3c9c61d9f..f3ec47a88b5 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 static void assertWindowTags(Timeline timeline, Object... expectedWindowT 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 static void assertWindowTags(Timeline timeline, Object... expectedWindowT 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 static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCo 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 6e0b0b40f2b..7e3862ca31e 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.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 991be9b08b8..2bed5d6f8bd 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.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 @@ final class Builder { private LoadControl loadControl; private BandwidthMeter bandwidthMeter; private Looper looper; + private AnalyticsCollector analyticsCollector; + private boolean useLazyPreparation; private boolean buildCalled; /** @@ -152,6 +155,8 @@ final class Builder { *

        • {@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 Builder(Context context, Renderer... renderers) { new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -180,6 +187,8 @@ public Builder(Context context, Renderer... renderers) { * @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 Builder( 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 Builder( this.loadControl = loadControl; this.bandwidthMeter = bandwidthMeter; this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; this.clock = clock; } @@ -251,6 +264,36 @@ public Builder setLooper(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 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 ba63dee80e6..b5956c9dae0 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 static final class Builder { private BandwidthMeter bandwidthMeter; private AnalyticsCollector analyticsCollector; private Looper looper; + private boolean useLazyPreparation; private boolean buildCalled; /** @@ -115,6 +116,7 @@ public static final class Builder { * 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 Builder(Context context, RenderersFactory renderersFactory) { DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -160,6 +163,7 @@ public Builder(Context context, RenderersFactory renderersFactory) { * @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 Builder( BandwidthMeter bandwidthMeter, Looper looper, AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, Clock clock) { this.context = context; this.renderersFactory = renderersFactory; @@ -178,6 +183,7 @@ public Builder( this.bandwidthMeter = bandwidthMeter; this.looper = looper; this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; this.clock = clock; } @@ -247,6 +253,23 @@ public Builder setAnalyticsCollector(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. 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 e37f192c97f..3a9d83769b2 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 ImaAdsLoader buildForAdsResponse(String adsResponse) { 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 void setPlayer(@Nullable Player player) { Assertions.checkState( player == null || player.getApplicationLooper() == Looper.getMainLooper()); nextPlayer = player; + wasSetPlayerCalled = true; } @Override @@ -585,9 +587,12 @@ public void setSupportedContentTypes(@C.ContentType int... contentTypes) { @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 void start(EventListener eventListener, AdViewProvider adViewProvider) { @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 8415f57c9a1..6b5f4a0002b 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 1392e782f84..462e6ea0449 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 @@ protected VpxDecoderException decode( } 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 cd862e503f4..d073eb4ee00 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 @@ private 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 @@ private 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 773959fbfca..8fd25f2cf91 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 final boolean isKeyFrame() { 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 c31ae92cfc9..7a19d85aa88 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 DecoderInputBuffer(@BufferReplacementMode int bufferReplacementMode) { 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 final boolean isEncrypted() { */ 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 c785865f6a7..e4f42fcf91c 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 MatroskaExtractor(@Flags int flags) { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); + blockAddData = new ParsableByteArray(); } @Override @@ -479,6 +493,8 @@ protected int getElementType(int id) { 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 @@ protected int getElementType(int id) { 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 @@ protected int getElementType(int id) { 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 @@ protected int getElementType(int id) { 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 @@ protected void integerElement(int id, long value) throws ParserException { 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 @@ protected void integerElement(int id, long value) throws ParserException { break; } break; + case ID_BLOCK_ADD_ID: + blockAddId = (int) value; + break; default: break; } @@ -1171,12 +1196,30 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 @@ private void commitSampleToOutput(Track track, long timeUs) { 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 @@ private void writeSampleData(ExtractorInput input, Track track, int size) // 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 @@ private static final class Track { 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 d07def1894e..c700259b13e 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 boolean isCodecSupported(Format format) { 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 ee2c9ad1a39..c077d8d227c 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 @@ private boolean feedInputBuffer() throws ExoPlaybackException { Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); + if (buffer.hasSupplementalData()) { + handleInputBufferSupplementalData(buffer); + } onQueueInputBuffer(buffer); if (bufferEncrypted) { @@ -1297,10 +1300,23 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) // 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 921afcdf2f4..fa4a26aa3ce 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 int read( 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 int read( } } + /** + * 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 de77e8318d1..03108008762 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.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 VideoDecoderException( private CodecMaxValues codecMaxValues; private boolean codecNeedsSetOutputSurfaceWorkaround; + private boolean codecHandlesHdr10PlusOutOfBandMetadata; private Surface surface; private Surface dummySurface; @@ -683,6 +685,8 @@ protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); + codecHandlesHdr10PlusOutOfBandMetadata = + Assertions.checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported(); } @Override @@ -727,6 +731,37 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) 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 @@ private static boolean isBufferVeryLate(long earlyUs) { 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 b4b09b20a28..10ccb4eba2b 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 6b5f4a0002b..dca6e13cc92 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 c452a29cf92..370d79edc75 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 @@ public void setIsTimestampMaster(boolean isTimestampMaster) { * 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 @@ public void getNextChunk( } 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 ff725ec6f7f..e4c756c6b64 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 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 @@ public void prepareWithMasterPlaylistInfo( 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 @@ public boolean continueLoading(long positionUs) { ? 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 3a9d83769b2..f1a4036038b 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 @@ private ImaAdsLoader( pendingContentPositionMs = C.TIME_UNSET; adGroupIndex = C.INDEX_UNSET; contentDurationMs = C.TIME_UNSET; + timeline = Timeline.EMPTY; } /** @@ -966,7 +967,7 @@ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason in if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); } - updateImaStateForPlayerState(); + onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @Override @@ -1021,7 +1022,7 @@ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { } } 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 void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { } } } - } 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 b4e00589822..65d997396b6 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.ByteOrder; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; /** @@ -122,7 +121,15 @@ public static class DefaultAudioProcessorChain implements AudioProcessorChain { * 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 c0b5205455b..7982163ee80 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 void setUp() { /* 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 c2007b19a39..6e7b19936f4 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 cacdaec02ed..38d66e5cbc3 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 @@ 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 @@ public int getPlaybackState() { @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 @@ public void prepare(MediaSource mediaSource) { @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 @@ public void setForegroundMode(boolean foregroundMode) { @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 @@ public void release() { getResetPlaybackInfo( /* resetPosition= */ false, /* resetState= */ false, + /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } @@ -572,11 +573,6 @@ public Timeline getCurrentTimeline() { 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 @@ private void handlePlaybackInfo( } 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 @@ private PlaybackInfo getResetPlaybackInfo( startPositionUs, contentPositionUs, playbackState, + resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, @@ -728,6 +728,7 @@ private static final class PlaybackInfoUpdate implements Runnable { 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 @@ public PlaybackInfoUpdate( 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 @@ public void run() { 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 53c381961e8..a6e8c679a8f 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 @@ // 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 @@ public boolean handleMessage(Message msg) { 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 @@ public boolean handleMessage(Message msg) { 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 @@ private void maybeNotifyPlaybackInfoChanged() { 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 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti /* 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 @@ private void stopInternal( /* 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 @@ private void releaseInternal() { /* resetRenderers= */ true, /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* resetState= */ true); + /* resetState= */ true, + /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -861,7 +867,8 @@ private void resetInternal( 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 @@ private void resetInternal( startPositionUs, contentPositionUs, playbackInfo.playbackState, + resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, @@ -1382,7 +1390,8 @@ private void handleSourceInfoRefreshEndedPlayback() { /* 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 e9b99acd778..9d2a3b54590 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 @@ 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 @@ public static PlaybackInfo createDummy( startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, Player.STATE_IDLE, + /* playbackError= */ null, /* isLoading= */ false, TrackGroupArray.EMPTY, emptyTrackSelectorResult, @@ -124,6 +128,7 @@ public PlaybackInfo( long startPositionUs, long contentPositionUs, @Player.State int playbackState, + @Nullable ExoPlaybackException playbackError, boolean isLoading, TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult, @@ -136,6 +141,7 @@ public PlaybackInfo( 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 @@ public PlaybackInfo copyWithNewPosition( positionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -217,6 +224,7 @@ public PlaybackInfo copyWithTimeline(Timeline timeline) { startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -240,6 +248,31 @@ public PlaybackInfo copyWithPlaybackState(int playbackState) { 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 @@ public PlaybackInfo copyWithIsLoading(boolean isLoading) { startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -288,6 +322,7 @@ public PlaybackInfo copyWithTrackInfo( startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -311,6 +346,7 @@ public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPerio 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 825424ae043..1ccd4ffc840 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 final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { @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 3c6c4462ca7..afcce904e93 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 @@ private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { /* 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 b7465f82eb0..03aabecb0e7 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 final O dequeueOutputBuffer() throws E { * * @param outputBuffer The output buffer being released. */ + @CallSuper protected void releaseOutputBuffer(O outputBuffer) { synchronized (lock) { releaseOutputBufferInternal(outputBuffer); @@ -150,6 +152,7 @@ public final void flush() { } } + @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 462e6ea0449..56300587126 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 @@ /* 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 f1a4036038b..3b2bcd8880e 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 ImaAdsLoader buildForAdsResponse(String adsResponse) { private int lastVolumePercentage; private AdsManager adsManager; + private boolean initializedAdsManager; private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; @@ -613,8 +614,8 @@ public void start(EventListener eventListener, AdViewProvider adViewProvider) { 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 void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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 void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason in 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 void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // Internal methods. - private void startAdPlayback() { + private void initializeAdsManager() { AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); @@ -1055,10 +1061,9 @@ private void startAdPlayback() { 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 @@ private void startAdPlayback() { 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 ab880703ee0..98aee17d737 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 void testStart_updatesAdPlaybackState() { 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 dca6e13cc92..5f7d5bea9cd 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 b43701f1bc3..21dff0b4b25 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 @@ protected static final class AudioTrackScore implements Comparable 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 6e7b19936f4..f297bb468e0 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.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 DataSpec( * @param key {@link #key}. * @param flags {@link #flags}. */ - @SuppressWarnings("deprecation") public DataSpec( Uri uri, @HttpMethod int httpMethod, @@ -243,6 +246,41 @@ public 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 DataSpec( this.length = length; this.key = key; this.flags = flags; + this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); } /** @@ -341,7 +380,8 @@ public DataSpec subrange(long offset, long length) { position + offset, length, key, - flags); + flags, + httpRequestHeaders); } } @@ -353,6 +393,14 @@ public DataSpec subrange(long offset, long length) { */ 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 00000000000..f6e30f814a9 --- /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 38b373b26cd..c36e62db862 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj 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 6e20db7bf70..541c3b2d9d0 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 long open(DataSpec dataSpec) throws IOException { } openNextSource(false); return bytesRemaining; - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -327,6 +327,9 @@ public int read(byte[] buffer, int offset, int readLength) throws IOException { } handleBeforeThrow(e); throw e; + } catch (Throwable e) { + handleBeforeThrow(e); + throw e; } } @@ -353,7 +356,7 @@ public void close() throws IOException { notifyBytesRead(); try { closeCurrentSource(); - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -520,7 +523,7 @@ private void closeCurrentSource() throws IOException { } } - 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 47470c5de75..6277ec686f9 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 static void remove(Cache cache, String key) { } } - /*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 81212b731f0..e618fcad75d 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 @@ private SimpleCacheSpan getSpan(String key, long 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. + 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 @@ private void removeSpanInternal(CacheSpan span) { } /** - * 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 10ccb4eba2b..3704a09da04 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 void init( 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 d6fe6e2dc11..f92662f2841 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.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.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 5f7d5bea9cd..c58841c2b32 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 ed925230171..bff1672e625 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 void clearAllRequestProperties() { 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 ec05c52f447..1a6a67b140e 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 Uri getUri() { 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 3ee1ef75646..e28c133a26d 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 Uri getUri() { 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 long open(DataSpec dataSpec) throws HttpDataSourceException { 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 17fb4ad7a1f..778c116e014 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 InvalidResponseCodeException( */ 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 c58841c2b32..41613f9cde7 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 c700259b13e..6dcbf896c9b 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 static MediaCodecInfo newPassthroughInstance(String name) { /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); } @@ -121,6 +151,9 @@ public static MediaCodecInfo newPassthroughInstance(String name) { * 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 static MediaCodecInfo newInstance( 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 static MediaCodecInfo newInstance( codecMimeType, capabilities, /* passthrough= */ false, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, forceSecure); } @@ -148,6 +187,9 @@ private 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 @@ private 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 9c42916cada..966e9fecc21 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 @@ private static ArrayList getDecoderInfosInternal( 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 @@ private static ArrayList getDecoderInfosInternal( mimeType, codecMimeType, capabilities, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { @@ -330,6 +336,9 @@ private static ArrayList getDecoderInfosInternal( 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 @@ private static void applyWorkarounds(String mimeType, List decod /* mimeType= */ MimeTypes.AUDIO_RAW, /* codecMimeType= */ MimeTypes.AUDIO_RAW, /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, /* forceDisableAdaptive= */ false, /* forceSecure= */ false)); } @@ -565,6 +577,69 @@ private static void applyWorkarounds(String mimeType, List decod } } + /** + * 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 c83214c8d53..6a652098416 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.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 @@ @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 interface ReleaseCallback { private final @DefaultDrmSessionManager.Mode int mode; @Nullable private final HashMap 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 DefaultDrmSession( } this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; - this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; this.eventDispatcher = eventDispatcher; + loadErrorHandlingPolicy = + new DefaultLoadErrorHandlingPolicy( + /* minimumLoadableRetryCount= */ initialDrmRequestRetryCount); state = STATE_OPENING; postResponseHandler = new PostResponseHandler(playbackLooper); } @@ -531,7 +544,7 @@ public void handleMessage(Message msg) { throw new RuntimeException(); } } catch (Exception e) { - if (maybeRetryRequest(msg)) { + if (maybeRetryRequest(msg, e)) { return; } response = e; @@ -539,23 +552,27 @@ public void handleMessage(Message msg) { postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget(); } - private boolean maybeRetryRequest(Message originalMsg) { + private boolean maybeRetryRequest(Message originalMsg, Exception e) { boolean allowRetry = originalMsg.arg1 == 1; if (!allowRetry) { return false; } int errorCount = originalMsg.arg2 + 1; - if (errorCount > 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 e28c133a26d..f38b0709f9a 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 @@ 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.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 @@ */ 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 void clearAllRequestProperties() { 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 void close() throws HttpDataSourceException { } } + /** 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 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { length, allowGzip, allowIcyMetadata, - /* followRedirects= */ true); + /* followRedirects= */ true, + dataSpec.httpRequestHeaders); } // We need to handle redirects ourselves to allow cross-protocol redirects. @@ -453,7 +468,8 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { 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 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { * @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 @@ private HttpURLConnection makeConnection( 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 00000000000..7c2b63c941b --- /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 39b92aa217e..6736ab4b163 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 7f775f4d325..031960636da 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 6ca2e7164ad..4d0832d1988 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 e6e874a27a8..831ad478310 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 be71884846a..b6c6064227e 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 85e60f27969..60b9cdbc4e0 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 d0e40990be0..0c5b5037f56 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 @@ 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.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 15cc9b6469f..7023693d032 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 @@ 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 f58389d9d4a..be08ca9ea28 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 839ed304bde..440a25a2893 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 @@ 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 d3c32ac9572..347f49e27ca 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.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.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 09fa62e51af..3920cd3a80e 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.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.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 f92662f2841..9e8009388eb 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.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 58dbec611a6..a0741b23cc7 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 bff1672e625..241b879b9a9 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 @@ 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 e22c97859ae..06b9fac4875 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.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 340e9832be1..e330b13e473 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 3b2bcd8880e..cf0ea79da6e 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.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 98aee17d737..2995df4ab42 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.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 370e5515e82..7eb8dac49fd 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 @@ 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 cb1788f2fca..31084769645 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.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.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 5c969dd44de..87b9447f7ce 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 @@ 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 d72f6ffddcc..d5fed2958b3 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 @@ 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 b89a6f4eab3..fc4cc11b589 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 @@ 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 dd4077964bd..13f020031c3 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 @@ 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 56300587126..f6b1ddcceab 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 4e983cccc7a..9dd24326221 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 @@ 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 d073eb4ee00..38553697d06 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.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 38d66e5cbc3..15a0e3a7bd0 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.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 a6e8c679a8f..8beb7d781ed 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.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 e515877d78d..22c4119021a 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 eed59876f92..f4d3dfdeda6 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 b5956c9dae0..795eca5ea0d 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.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 57d3d8bf1d3..0018a15157a 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 1ccd4ffc840..43154a4b3f0 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 be62ad99d26..656548df475 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 183a74544dd..44f8c10afea 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 ed127bc5509..bd8fb213eda 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 6444b4747f8..8b9f1d1ced1 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 6a652098416..a667c8374a0 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.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 34fd223c645..042a30e440c 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.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 7cc2231e0f0..2f0246ba646 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 @@ 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 e77504c91cb..15a4b3da4ab 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.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 23b2300dfab..19c32daf614 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 @@ 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 05dab7e42d7..03c199bda09 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.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 9fed3b38e8b..004f873a33d 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 e4f42fcf91c..56ba01b967d 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 b3c26246e6e..2800c6069ea 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 @@ 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 5eaa5d5d319..f6e0fb8bc92 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 15c37430bcf..24d17f49565 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 39e74ae6a25..4ad9adfa2a1 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 d198e816d51..04dd7df3854 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 @@ 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 536a31c9fca..af272352572 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 6dcbf896c9b..1726648793a 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.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 966e9fecc21..eb97d546e5a 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.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 54360f8f6b8..de6b3f447ed 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.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 5326220452a..808df6d36fc 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 703f8bafc0e..4780c075d5a 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 35800f56dad..671bc9eb743 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 2b90fac6ab1..11947218a33 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 fc07a318b1a..c4c8647a551 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 a7ab93a6dd4..51aec3638f4 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 @@ 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 39359a9367f..3dea9417c58 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 @@ 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 a0201e19e66..fc1f0e2bdc2 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 e305259cbc7..8da37e7f8f3 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 eb2b704bee9..8d1b743a6dc 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 3b4d061aaaa..3365749e1ab 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 @@ 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 9fdcc48c128..e90b0991736 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 c5d0526eb59..9a5ac40a056 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 ded7ef73ff6..2bd1af01e88 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 2361c9729fa..bb639bfb7d8 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.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 9826c5b137b..b850a08aebf 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 21dff0b4b25..4e8da959ba5 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.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 5587af9cbfb..425da6c1c40 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 c406f262d1f..1582fabc887 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 @@ 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 94a6e21c86a..55c580ead23 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 @@ 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 67dd9a7a551..da120414b40 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.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 f38b0709f9a..0cf54a153c8 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 778c116e014..4d3aca570eb 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 ff032a4ed01..fbfd6986108 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.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 6277ec686f9..bbec189b4d8 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 3749ecfc9a6..22086f8ac8f 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.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 b4ccc5bcc49..9a4891d3296 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 16a891dbc6a..f4f143f3b01 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 cde9a351d99..c37e98776e0 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 1eb09778475..a29460b84c3 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 61457c308da..de30cfd2145 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 60f4fa17dd8..90be8660c66 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 35a36c3cd56..5c508490106 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.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 920d569fd3a..d1f874b428d 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.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 03108008762..10827f8aa77 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.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 ad59d11c4a6..83f528e3229 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 @@ 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 caa79d7c183..bf31ce2abb1 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.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 2f76a2c23db..70f30d32804 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 @@ 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 f924dfb34ce..013acc5ee2d 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 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 875f8b5d7ba..ae87201c16e 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 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 cee5703ff88..e28277d945b 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.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 21fd43da214..d62474cb1b5 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 1b163c02e2a..c9a039a4bd4 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 @@ 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 9f6cce672e6..42436d95934 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 6dd4ade5906..d5b9ca478e5 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 ad2a3ad2650..7d47cc43c6b 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 2ba3b45ca05..f26a9b8e9a2 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 @@ 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 a89e907a373..8fa79befca3 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 030520f8cb8..21c29c6d872 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 c08a867f37b..d395e95fd92 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 268219b6d5d..a39a0bab4f5 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 @@ 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 69a2cf96be7..d0e621547e8 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.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.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 3a194e091a6..07651053142 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.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.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 c69cb217044..8ad00e1ddad 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.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.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 0d66922cabb..67701cbb93b 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.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.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 955fab14c90..e55666e178f 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 0bdc1acc885..5fa2e4816b0 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.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 d62f2494d54..9e3f2e42ff4 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 0df3fff5fe0..f8a016bc8b2 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 @@ 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 02ed0a534ec..79990e53a62 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 @@ 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.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 6ef9d4907d0..ed9be4ea7e6 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.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 9ff6fcaf1f4..37ac8e98d0e 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.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 80de4181997..7276953cf5c 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.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 67bc9925581..419f31436af 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.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 5f3a5275c19..66a0f200914 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 @@ 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 5d07f986d21..c4116c3696d 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 735156e64c4..1b0a00624e9 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 211e85d30c2..29d1b0bd756 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.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 f454e28c68f..6df783da78e 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 @@ public long getNextFrameFirstSampleIndex() { * @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 @@ private native int flacDecodeToArray(long context, byte[] outputArray) 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 cd91b062888..03416abd06b 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 @@ private static FlacBinarySearchSeeker outputSeekMap( 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 boolean isSeekable() { @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 d60a7cead2a..0bea30752a3 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 830f3e2178a..dc0a6859515 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 fd3e36a806a..ac6e3e26d84 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 ef6eedbe1c2..1284e8febb2 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 00000000000..d1ab6b1af50 --- /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 b5e68187bed..5c6731c17b1 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 1b0a00624e9..e8abfdb73f0 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 Builder prepareSource(MediaSource mediaSource) { /** * 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 bcb97be2871..6d141fe04ab 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 @@ /** * 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 0cf54a153c8..c5b3c0b08b8 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 void close() throws HttpDataSourceException { } } - /** 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 @@ private HttpURLConnection makeConnection( 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 c5b3c0b08b8..37329a4868f 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 @@ * 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 void clearAllRequestProperties() { /** * 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 1284e8febb2..03b3199e216 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 d25fff51042..41fefafab7d 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 @@ public Extractor selectExtractor(ExtractorInput input, ExtractorOutput output, U 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 10827f8aa77..f73dde58c3f 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 @@ protected int supportsFormat( 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 @@ protected int supportsFormat( 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 8ad00e1ddad..e4fcb37af3f 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 @@ protected int[] getActionIndicesForCompactView(List actionNames, Player 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 f2259e8f1a3..fa40f0f0129 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 OutputStream startWrite() throws IOException { } 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 07651053142..e938192d8e8 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 interface ProgressUpdateListener { 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 interface ProgressUpdateListener { 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 PlayerControlView( 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 @@ private void updateShuffleButton() { 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 00000000000..283ce30c3c3 --- /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 3432935d5f7..293d1e7510a 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 @@ long getBlacklistDurationMsFor( * * @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 9510b8442ec..77609df69bd 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 37d8fbbb995..96c699b2e66 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 124555d9b5a..289fa1dc831 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 be08ca9ea28..06c734986c4 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 4af8f94c583..0d7d96db4c8 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 9c49ba94e15..9b618cd036f 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 2b5a6010a97..657fa75c24c 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 dfac2e1c26b..5d68711aa71 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 1031d6f4b76..f8992616a27 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 e330b13e473..e2292aed8f8 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 ecaa78e25b1..f0be172c901 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 68bd422185a..b0d9fb0187e 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 7b621a8df9c..2759299d63a 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 9c709305bf0..88d3524d720 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 3b8271869be..e40d6e02d78 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 d6aee8a35dc..afccef3e247 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 c64da2b86d8..ac90d64c1e9 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 0f685c11301..07696c1c26d 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 b16157f49bd..4fe2fae3282 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 5b3123e302d..3735c94096b 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 5865d3c36dd..d77944cf23c 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 5c6731c17b1..3c7b13a6a89 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 60b9cdbc4e0..ea058d95d42 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 77609df69bd..61f31ca35e4 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 7ee973723c4..537c5ba5345 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 3735c94096b..509dd22a282 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 61f31ca35e4..69726d9ed87 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 9b618cd036f..d5b7a99f96d 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 ea7564316f8..6a7aa107228 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 a667c8374a0..23c5f0a7556 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.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 interface ReleaseCallback { /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; - /* package */ final PostResponseHandler postResponseHandler; + /* package */ final ResponseHandler responseHandler; private @DrmSession.State int state; private int referenceCount; @Nullable private HandlerThread requestHandlerThread; - @Nullable private PostRequestHandler postRequestHandler; + @Nullable private RequestHandler requestHandler; @Nullable private T mediaCrypto; @Nullable private DrmSessionException lastException; private byte @NullableType [] sessionId; @@ -180,7 +181,7 @@ public DefaultDrmSession( new DefaultLoadErrorHandlingPolicy( /* minimumLoadableRetryCount= */ initialDrmRequestRetryCount); state = STATE_OPENING; - postResponseHandler = new PostResponseHandler(playbackLooper); + responseHandler = new ResponseHandler(playbackLooper); } public boolean hasSessionId(byte[] sessionId) { @@ -201,7 +202,7 @@ public void onMediaDrmEvent(int what) { public void provision() { currentProvisionRequest = mediaDrm.getProvisionRequest(); - Util.castNonNull(postRequestHandler) + Util.castNonNull(requestHandler) .post( MSG_PROVISION, Assertions.checkNotNull(currentProvisionRequest), @@ -254,7 +255,7 @@ public void acquireReference() { Assertions.checkState(state == STATE_OPENING); requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); + requestHandler = new RequestHandler(requestHandlerThread.getLooper()); if (openInternal(true)) { doLicense(true); } @@ -266,9 +267,9 @@ 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(responseHandler).removeCallbacksAndMessages(null); + Util.castNonNull(requestHandler).removeCallbacksAndMessages(null); + requestHandler = null; Util.castNonNull(requestHandlerThread).quit(); requestHandlerThread = null; mediaCrypto = null; @@ -417,7 +418,7 @@ private void postKeyRequest(byte[] scope, int type, boolean allowRetry) { try { currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, optionalKeyRequestParameters); - Util.castNonNull(postRequestHandler) + Util.castNonNull(requestHandler) .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); } catch (Exception e) { onKeysError(e); @@ -490,9 +491,9 @@ private boolean isOpen() { // Internal classes. @SuppressLint("HandlerLeak") - private class PostResponseHandler extends Handler { + private class ResponseHandler extends Handler { - public PostResponseHandler(Looper looper) { + public ResponseHandler(Looper looper) { super(looper); } @@ -516,29 +517,30 @@ public void handleMessage(Message msg) { } @SuppressLint("HandlerLeak") - private class PostRequestHandler extends Handler { + private class RequestHandler extends Handler { - public PostRequestHandler(Looper backgroundLooper) { + public RequestHandler(Looper backgroundLooper) { super(backgroundLooper); } void post(int what, Object request, boolean allowRetry) { - int allowRetryInt = allowRetry ? 1 : 0; - int errorCount = 0; - obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget(); + RequestTask requestTask = + new RequestTask(allowRetry, /* startTimeMs= */ SystemClock.elapsedRealtime(), request); + obtainMessage(what, requestTask).sendToTarget(); } @Override public void handleMessage(Message msg) { - Object request = msg.obj; + RequestTask requestTask = (RequestTask) msg.obj; Object response; try { switch (msg.what) { case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request); + response = + callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request); break; case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) request); + response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request); break; default: throw new RuntimeException(); @@ -549,30 +551,47 @@ public void handleMessage(Message msg) { } response = e; } - postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget(); + responseHandler + .obtainMessage(msg.what, Pair.create(requestTask.request, response)) + .sendToTarget(); } private boolean maybeRetryRequest(Message originalMsg, Exception e) { - boolean allowRetry = originalMsg.arg1 == 1; - if (!allowRetry) { + RequestTask requestTask = (RequestTask) originalMsg.obj; + if (!requestTask.allowRetry) { return false; } - int errorCount = originalMsg.arg2 + 1; - if (errorCount > 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 e938192d8e8..ceb131cc290 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 interface ProgressUpdateListener { 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 PlayerControlView( } 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 @@ private void setButtonEnabled(boolean enabled, View view) { 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 9b374d8382d..9bd616583e0 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 ceb131cc290..fd949db6a25 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 interface ProgressUpdateListener { 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 PlayerControlView( 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 @@ private void updateRepeatModeButton() { } if (player == null) { setButtonEnabled(false, repeatToggleButton); + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); return; } setButtonEnabled(true, repeatToggleButton); @@ -831,10 +838,15 @@ private void updateShuffleButton() { } 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 027e57ee928..acfddf11462 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 8a983c543a9..fa630292a9c 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 f56a6c06bfd..b754aa90ea1 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 91063e1a54f..87cad1be251 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 0f5fbe3f4d8..ddfc6537316 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 16300747f7a..73c4223d8c0 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 6a33be2a8f4..7187494eca9 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 511a5e4f198..f7dcd29e49a 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 cca445feca6..6ccd22744c6 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 24fb7b2b3b4..a9a960285f2 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 3b48eab3b81..39a3ce85c99 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 1568074f9f2..1ad837b32d7 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 19b0f094467..2bef98e781c 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 1bb620dd2ba..e06459de8db 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 1ddbe4a5fae..47144dc00cb 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 cf25e2ada0b..62125c52269 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 cf25e2ada0b..62125c52269 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 cf25e2ada0b..62125c52269 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 ceeb0b84970..beeeba4e9c4 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 0118da57bea..e880d66bf0d 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 99ca9548ed5..515c6651815 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 4d992fee0f1..3f3d75d4f8f 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 fed94b55697..dfc74a9c212 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 0dc2f9d3464..c0e53c437bd 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 0f3534924f4..ef42066df3c 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 46c07f531e8..057a6a8f676 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 e6689353f10..419ea0c5525 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 488eb39f6a0..daec2b447d9 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 8ba92054ffd..0435e3eb849 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 4fa19429864..b36c9ff9e72 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 baf77650e07..ad67165cf89 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 04a2aeb1402..31f4db37d20 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 7410576e81b..d7bae9719d0 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 bdb27a6648a..4c09db5251f 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 ffa05821e97..e10a62a11b8 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 695196c5be6..8dd08278a32 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 b4158736a81..dc479596b96 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 13ceaaf51f9..7b9ecc7a3a8 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 92119d1fe5b..ef2c1ab2b7a 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 62728de0269..3636a6e6d61 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 6e6bfcb165f..85df144fca9 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 230660ad6ae..3442318047b 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 57b8bbb5f50..a4c5b36a6c0 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 d7996610b2c..8d380f28084 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 3e9a63dc997..1b3cfe4573c 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 59b541808ab..6d7a232bcc2 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 08a54d7240d..1ad12a14d7e 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 6e79db09039..a227434e7a5 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 383d1025208..8b8df3f9d41 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 a0900ab8514..5c2bbc738c1 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 6dab5be8de8..8bc50c76058 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 b30b76d5164..e8a88a312df 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 f2847dd8297..f9a0850bec5 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 ff56480df11..f633a13af4b 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 3fbf113f1ee..4c718151367 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 9f257598788..0d30c2c5198 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 8df3b62b0c7..46f76e975a5 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 188e18f6b5e..60df32be818 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 9e83387a76c..63f3abd3430 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 9bb8cfc8eeb..b7f5a6b63e2 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 e66a282da4e..c72ea716bfa 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 b6bfb1848f4..19d37854fd3 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 6d5ddaea28b..c45fd13dcfd 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 1e3adff704d..17f1e66764b 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 d5b8903ed7a..950c867e8b3 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 b45fd8ab035..6c3074bc418 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 7af95a46326..c7dafaf786b 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 1cdd3252780..66568a3acc6 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 2b2b9e13d6f..a4544299c0d 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 ea344b03456..8fcb29cc2f1 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 3cd933ccf15..918b62f0999 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 21852c5011c..df00a07299c 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 2fbf36514f1..5005f0bfb94 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 5d338b61af2..a42a8128b32 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 aa98b0728ed..47f35d1bae8 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 2dcf5a518de..3d8e270636c 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 1cdb249ef0b..dc78b504fd4 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 fe21669ea49..d2c3fb93ca6 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 56e0a1a53b3..d040db1b03a 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 7b29f7924ed..c3a1b5521ec 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 83cf9b2e972..08922a5037b 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 bbb4aca8d5b..e3f1c3aaeca 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 e73524815a9..c458a3ea99c 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 50fdb68f30f..2708596a9e4 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 cd39c9538aa..14fe81f6055 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 @@ private RepresentationHolder( // 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 00000000000..7510fdadc09 --- /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 eb983e7524b..11b493d5554 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 91bceb9aee6..1903e339953 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 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 @@ private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOExcep 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 synchronized void onRedirectReceived( 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 2a8f6715770..3053961f491 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 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 @@ private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException { 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 7cb767854f2..c768fe49818 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 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 @@ interface Listener { */ 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 @@ private DataSpec buildDataSpec(long position) { 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 @@ public int hashCode() { 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 f5802bed4e7..acf55504273 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 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 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 DataSpec withUri(Uri uri) { 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 37329a4868f..436cad0d645 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 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 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { 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 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { position, length, allowGzip, - allowIcyMetadata, /* followRedirects= */ true, dataSpec.httpRequestHeaders); } @@ -460,7 +457,6 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { position, length, allowGzip, - allowIcyMetadata, /* followRedirects= */ false, dataSpec.httpRequestHeaders); int responseCode = connection.getResponseCode(); @@ -502,7 +498,6 @@ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { * @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 @@ private HttpURLConnection makeConnection( long position, long length, boolean allowGzip, - boolean allowIcyMetadata, boolean followRedirects, Map requestParameters) throws IOException { @@ -541,14 +535,10 @@ private HttpURLConnection makeConnection( } 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 f6e30f814a9..2323dfe9650 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 void httpRequestParameters_areReadOnly() { } @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); + } + + @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; + } - dataSpecCopy = dataSpec.subrange(2, 2); - assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(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 fc3783d56b9..eae21b5b306 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 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection( 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 a369b077f4b..d488f4113e6 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 11b493d5554..664a22e15ef 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 759f88cdab9..6c90a3660df 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.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 @@ private MissingSchemeDataException(UUID uuid) { /* 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 0d966c90807..fb64a7d13b4 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.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 @@ protected DrmSessionManager buildDrmSessionManager( 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 | 156 +-- .../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 | 1082 ++++------------- .../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, 742 insertions(+), 3842 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 a9d1db64ad2..347f49e27ca 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.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.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 @@ private void initializePlayer() { Intent intent = getIntent(); releaseMediaDrms(); - mediaSources = createTopLevelMediaSources(intent); - if (mediaSources.isEmpty()) { + mediaSource = createTopLevelMediaSource(intent); + if (mediaSource == null) { return; } @@ -388,12 +388,12 @@ private void initializePlayer() { 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 @@ private List createTopLevelMediaSources(Intent intent) { } } - 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 @@ private void releasePlayer() { 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 91a4c86cf2f..2210f2c8b2c 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 PendingResult loadItems( 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 @@ private void updateInternalState() { 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 a6a725ee9e9..a9572b7a8d9 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 @@ 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 @@ public FakePlayer() { 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 a5c6b006194..2995df4ab42 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 TestAdsLoaderListener( 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 4418549c8b2..7c8a454191f 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.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.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 @@ final class Builder { 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 Builder(Context context, Renderer... renderers) { new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), Util.getLooper(), - /* analyticsCollector= */ null, + new AnalyticsCollector(Clock.DEFAULT), /* useLazyPreparation= */ true, Clock.DEFAULT); } @@ -199,7 +197,7 @@ public Builder( 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 ExoPlayer build() { 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 - 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}. - * - * @param mediaItems The new {@link MediaSource MediaSources}. + * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code + * prepare(mediaSource, true, true)}. */ - void setMediaItems(List mediaItems); + void prepare(MediaSource mediaSource); /** - * 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}. + * @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 b900491b1d4..efe351c70ac 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 static SimpleExoPlayer newSimpleInstance( drmSessionManager, bandwidthMeter, analyticsCollector, - /* useLazyPreparation= */ true, Clock.DEFAULT, looper); } @@ -345,13 +344,6 @@ public static ExoPlayer newInstance( 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 b9f29e2cb35..cbbf5cacbca 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.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.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 @@ 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 @@ * @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 @@ public ExoPlayerImpl( 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 @@ public ExoPlayerImpl( 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 @@ public void handleMessage(Message msg) { }; playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); pendingListenerNotifications = new ArrayDeque<>(); - if (analyticsCollector != null) { - analyticsCollector.setPlayer(this); - } internalPlayer = new ExoPlayerImplInternal( renderers, @@ -157,7 +139,6 @@ public void handleMessage(Message msg) { playWhenReady, repeatMode, shuffleModeEnabled, - analyticsCollector, eventHandler, clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); @@ -231,168 +212,41 @@ public ExoPlaybackException getPlaybackError() { } @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 @@ public void setForegroundMode(boolean foregroundMode) { @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 @@ public void stop(boolean reset) { playbackInfo, /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, - TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + TIMELINE_CHANGE_REASON_RESET, /* seekProcessed= */ false); } @@ -574,11 +432,13 @@ public void release() { 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 @@ public Timeline getCurrentTimeline() { // 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 @@ private void handlePlaybackInfo( 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 @@ private PlaybackInfo getResetPlaybackInfo( 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 @@ private PlaybackInfo getResetPlaybackInfo( 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 @@ private void updatePlaybackInfo( /* 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 @@ private static final class PlaybackInfoUpdate implements Runnable { 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 @@ public PlaybackInfoUpdate( 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 9cad08ab7b1..9d0692cf0e9 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.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 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 @@ implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - Playlist.PlaylistInfoRefreshListener, + MediaSourceCaller, PlaybackParameterListener, PlayerMessage.Sender { @@ -72,21 +71,16 @@ 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 @@ 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 @@ 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 @@ public ExoPlayerImplInternal( boolean playWhenReady, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, - @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock) { this.renderers = renderers; @@ -176,14 +170,12 @@ public ExoPlayerImplInternal( 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 @@ public void stop(boolean reset) { 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 @@ public Looper getPlaybackLooper() { 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 @@ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // 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 @@ public boolean handleMessage(Message msg) { 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 @@ public boolean handleMessage(Message msg) { 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 @@ private void maybeNotifyPlaybackInfoChanged() { } } - 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 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti long periodPositionUs; long contentPositionUs; boolean seekPositionAdjusted; - @Nullable Pair resolvedSeekPosition = resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (resolvedSeekPosition == null) { @@ -821,7 +689,7 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti } 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 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti 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 @@ private void stopInternal( 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 @@ private void stopInternal( 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 @@ private void releaseInternal() { 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 @@ private void resetInternal( 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 @@ private void resetInternal( } } - 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 @@ private void resetInternal( 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 @@ private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackExcept 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 @@ private void maybeThrowSourceInfoRefreshError() throws IOException { } } } - 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 @@ private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackEx long newContentPositionUs = oldContentPositionUs; if (pendingInitialSeekPosition != null) { // Resolve initial seek position. - @Nullable Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; @@ -1523,12 +1404,12 @@ private void handleSourceInfoRefreshEndedPlayback() { 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 @@ private void handleSourceInfoRefreshEndedPlayback() { * @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 @@ private Pair getPeriodPosition( } 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 @@ private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException rendererCapabilities, trackSelector, loadControl.getAllocator(), - playlist, + mediaSource, info, emptyTrackSelectorResult); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); @@ -1661,7 +1545,7 @@ private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException 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 @@ private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException } private void maybeUpdateReadingPeriod() throws ExoPlaybackException { - @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (readingPeriodHolder == null) { return; } @@ -2079,38 +1963,14 @@ public int compareTo(@NonNull PendingMessageInfo other) { } } - 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; - - 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 final MediaSource source; + public final Timeline timeline; - 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 @@ private static final class PlaybackInfoUpdate { 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 @@ public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReaso 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 5bbbcbea2a9..850d2b7d108 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 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 @@ 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 @@ * @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 @@ public MediaPeriodHolder( 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 @@ public MediaPeriodHolder( 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 @@ public long applyTrackSelection( /** 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 @@ private boolean isLoadingMediaPeriod() { /** 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 @@ private static MediaPeriod createMediaPeriod( /** 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 5b39db54aa3..901b7b4d94b 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 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 @@ public boolean shouldLoadNextMediaPeriod() { * @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 @@ public MediaPeriodHolder enqueueNextMediaPeriodHolder( RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, - Playlist playlist, + MediaSource mediaSource, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = @@ -157,7 +158,7 @@ public MediaPeriodHolder enqueueNextMediaPeriodHolder( 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 b9ab69c45f7..fafbd25c32f 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 @@ default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reas * {@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 void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { 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 c5476a151b3..00000000000 --- 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 de9802357cb..43a5ebab99a 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.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 Builder(Context context, RenderersFactory renderersFactory) { * @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 SimpleExoPlayer build() { loadControl, bandwidthMeter, analyticsCollector, - useLazyPreparation, clock, looper); } @@ -343,6 +339,7 @@ public SimpleExoPlayer build() { 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 SimpleExoPlayer build() { * @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 @@ protected SimpleExoPlayer( LoadControl loadControl, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { this( @@ -385,14 +378,26 @@ protected SimpleExoPlayer( 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 @@ protected SimpleExoPlayer( @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, AnalyticsCollector analyticsCollector, - boolean useLazyPreparation, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; @@ -435,15 +439,7 @@ protected SimpleExoPlayer( // 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 ExoPlaybackException getPlaybackError() { } @Override - @Deprecated public void retry() { verifyApplicationThread(); - prepare(); - } - - @Override - public void prepare() { - verifyApplicationThread(); - @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); - player.prepare(); + if (mediaSource != null + && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } } @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 SeekParameters getSeekParameters() { @Override public void setForegroundMode(boolean foregroundMode) { - verifyApplicationThread(); player.setForegroundMode(foregroundMode); } @@ -1317,6 +1211,13 @@ public void setForegroundMode(boolean foregroundMode) { 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 void release() { } 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 458532c86d2..c496052f94d 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 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 long getPositionInFirstPeriodUs() { 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 long getAdResumePositionUs() { 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 2cb160d092e..43154a4b3f0 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 final void notifySeekStarted() { } } - /** 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 void onSeekProcessed() { /** 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 void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { 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 a307e4b35d1..29ef1faa80e 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 AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) } @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 int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode // 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 int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode } @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 int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeat // 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 final Period getPeriod(int periodIndex, Period period, boolean setIds) { 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 final int getIndexOfPeriod(Object uid) { 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 final Object getUidOfPeriod(int periodIndex) { 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 cfd0ad93774..8dfea1e5116 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.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 68bed250e86..ac23e2a8317 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 33bbf795be2..8727fc5ed9e 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 @@ 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 MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { } /** Returns the {@link Timeline}. */ - public synchronized Timeline getTimeline() { + public Timeline getTimeline() { return timeline; } @@ -130,7 +129,7 @@ public void releaseSourceInternal() { } @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 Object getUidOfPeriod(int periodIndex) { } /** 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 int getPeriodCount() { @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 66e78eb3a5c..c37e98776e0 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 @@ private static String getDiscontinuityReasonString(@Player.DiscontinuityReason i 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 cbd246cf221..e11aa53b0f2 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.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 @@ private static String normalizeLanguageCodeSyntaxV21(String languageTag) { } } - /** - * 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 ee51a280522..37c026db74e 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.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 void setUp() { @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 void testPlayEmptyTimeline() throws Exception { .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 void testPlaySinglePeriodTimeline() throws Exception { .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 void testPlayMultiPeriodTimeline() throws Exception { 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 void testPlayShortDurationPeriods() throws Exception { 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 boolean isEnded() { 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 synchronized void prepareSourceInternal( @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 synchronized void prepareSourceInternal( // 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 synchronized void prepareSourceInternal( // 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 void testRepeatModeChanges() throws Exception { 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 void testRepeatModeChanges() throws Exception { 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 void testShuffleModeEnabledChanges() throws Exception { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(renderer) .setActionSchedule(actionSchedule) .build(context) @@ -470,13 +443,12 @@ public void testAdGroupWithLoadErrorIsSkipped() throws Exception { .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 void onSeekProcessed() { } @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 @@ protected FakeMediaPeriod createFakeMediaPeriod( .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -667,7 +634,7 @@ protected FakeMediaPeriod createFakeMediaPeriod( }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -693,7 +660,7 @@ protected FakeMediaPeriod createFakeMediaPeriod( }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .build(context) .start() .blockUntilEnded(TIMEOUT_MS); @@ -711,7 +678,7 @@ public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Ex FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -740,7 +707,7 @@ public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Ex FakeTrackSelector trackSelector = new FakeTrackSelector(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .build(context) @@ -777,7 +744,7 @@ public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemad .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -816,7 +783,7 @@ public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReuse .build(); new Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderers(videoRenderer, audioRenderer) .setTrackSelector(trackSelector) .setActionSchedule(disableTrackAction) @@ -840,35 +807,31 @@ public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReuse @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 void testResetMediaItemsWithPositionResetAndShufflingUsesFirstPeriod() th .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 @@ protected FakeMediaPeriod createFakeMediaPeriod( .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete()) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -981,10 +942,8 @@ public void run(SimpleExoPlayer player) { .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 void run(SimpleExoPlayer player) { .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 void run(SimpleExoPlayer player) { .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 void testStopWithResetReleasesMediaSource() throws Exception { } @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,103 +1072,62 @@ public void run(SimpleExoPlayer player) { .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); - testRunner.assertPlayedPeriodIndices(0, 1); + Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, + Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); } @Test - public void testResetPlaylistWithPreviousPosition() throws Exception { - Object firstWindowId = new Object(); - Timeline timeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); - Timeline firstExpectedMaskingTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ firstWindowId); - Object secondWindowId = new Object(); - Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId)); - Timeline secondExpectedMaskingTimeline = - new MaskingMediaSource.DummyTimeline(/* tag= */ secondWindowId); - MediaSource secondSource = new FakeMediaSource(secondTimeline); - AtomicLong positionAfterReprepare = new AtomicLong(); + 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("testResetPlaylistWithPreviousPosition") - .pause() + new ActionSchedule.Builder("testSeekAfterStopWithReset") .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .setMediaItems(/* windowIndex= */ 0, /* positionMs= */ 2000, secondSource) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(SimpleExoPlayer player) { - positionAfterReprepare.set(player.getCurrentPosition()); - } - }) - .play() + .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() - .blockUntilActionScheduleFinished(TIMEOUT_MS) .blockUntilEnded(TIMEOUT_MS); - - testRunner.assertTimelinesSame( - firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, 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()).isAtLeast(2000L); + 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 testResetPlaylistStartsFromDefaultPosition() throws Exception { - Object firstWindowId = new Object(); + public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception { Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId)); - Timeline firstExpectedDummyTimeline = - 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 secondExpectedDummyTimeline = - 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("testResetPlaylistStartsFromDefaultPosition") + new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000) - .setMediaItems(secondSource) - .waitForTimelineChanged( - secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) + .waitForTimelineChanged(secondTimeline) .executeRunnable( new PlayerRunnable() { @Override @@ -1246,14 +1146,8 @@ public void run(SimpleExoPlayer player) { .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); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + assertThat(positionAfterReprepare.get()).isAtLeast(2000L); } @Test @@ -1274,10 +1168,8 @@ public void testStopDuringPreparationOverwritesPreparation() throws Exception { .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 void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { .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 void testReprepareAfterPlaybackError() throws Exception { .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 void testReprepareAfterPlaybackError() throws Exception { } 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 void run(SimpleExoPlayer player) { 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 void run(SimpleExoPlayer player) { } 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 void run(SimpleExoPlayer player) { } }) .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 void run(SimpleExoPlayer player) { }) .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -1464,7 +1377,7 @@ public void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception 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 void run(SimpleExoPlayer player) { windowIndexHolder[0] = player.getCurrentWindowIndex(); } }) - .prepare() + .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_BUFFERING) .executeRunnable( new PlayerRunnable() { @Override @@ -1489,6 +1403,7 @@ public void run(SimpleExoPlayer player) { // 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 void run(SimpleExoPlayer player) { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(firstMediaSource) + .setTimeline(timeline) .setActionSchedule(actionSchedule) .build(context); try { @@ -1532,8 +1447,7 @@ public void playbackErrorAndReprepareWithPositionResetKeepsWindowSequenceNumber( .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 void onPlayerStateChanged( }; ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .setAnalyticsListener(listener) .build(context); @@ -1566,15 +1480,14 @@ public void onPlayerStateChanged( @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 void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { } 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 void testSendMessagesAfterPreparation() throws Exception { 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 void testSendMessagesAtStartAndEndOfPeriod() throws Exception { 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 void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exceptio 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 void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Excep 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 void testSendMessagesMoveCurrentWindowIndex() throws Exception { 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 void testSendMessagesMultiWindowDuringPreparation() throws Exception { 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 void testSendMessagesMultiWindowAfterPreparation() throws Exception { 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 void testSendMessagesMoveWindowIndex() throws Exception { 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 void testSendMessagesNonLinearPeriodOrder() throws Exception { .play() .build(); new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2152,21 +2059,16 @@ public void testTimelineUpdateDropsPrebufferedPeriods() throws Exception { /* 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 void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNu .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2326,56 +2228,6 @@ public void run(SimpleExoPlayer player) { 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 void run(SimpleExoPlayer player) { .build(); new ExoPlayerTestRunner.Builder() .setClock(clock) - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2462,7 +2314,7 @@ public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackG 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 void maybeThrowSourceInfoRefreshError() throws IOException { 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 void maybeThrowSourceInfoRefreshError() throws IOException { 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 void removingLoopingLastPeriodFromPlaylistDoesNotThrow() throws Exception .executeRunnable(concatenatingMediaSource::clear) .build(); new Builder() - .setMediaSources(concatenatingMediaSource) + .setMediaSource(concatenatingMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2611,7 +2505,7 @@ public void seekToUnpreparedWindowWithNonZeroOffsetInConcatenationStartsAtCorrec .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 void run(SimpleExoPlayer player) { .play() .build(); new Builder() - .setMediaSources(concatenatedMediaSource) + .setMediaSource(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2656,7 +2550,7 @@ public void seekToUnpreparedWindowWithMultiplePeriodsInConcatenationStartsAtCorr .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 void run(SimpleExoPlayer player) { .play() .build(); new Builder() - .setMediaSources(concatenatedMediaSource) + .setMediaSource(concatenatedMediaSource) .setActionSchedule(actionSchedule) .build(context) .start() @@ -2772,10 +2666,10 @@ public void run(SimpleExoPlayer player) { 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 void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { .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 b137cd3cff4..afcce904e93 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 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 void setUp() { @Test public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupTimeline(); + setupTimeline(/* initialPositionUs= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, @@ -83,7 +80,7 @@ public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { @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 void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos( @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 void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos @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 void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPer @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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP @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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP @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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaP 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 @@ private void setupTimeline(long... adGroupTimesUs) { /* 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 @@ private void enqueueNext() { rendererCapabilities, trackSelector, allocator, - playlist, + mediaSource, getNextMediaPeriodInfo(), new TrackSelectorResult( new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); @@ -427,6 +421,11 @@ private void setAdGroupFailedToLoad(int adGroupIndex) { 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 cc551db8ac0..00000000000 --- 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 ba05af385a7..d6e65cb34d5 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 void testMultiPeriodTimeline() { 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 7117f426f39..fb3e0936ae0 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.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 void testEmptyTimeline() throws Exception { 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 void testSinglePeriod() throws Exception { 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 void testAutomaticPeriodTransition() throws Exception { 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 void testAutomaticPeriodTransition() throws Exception { 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 void testPeriodTransitionWithRendererChange() throws Exception { 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 void testPeriodTransitionWithRendererChange() throws Exception { 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 void testSeekToOtherPeriod() throws Exception { 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 void testSeekToOtherPeriod() throws Exception { 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 void testSeekBackAfterReadingAhead() throws Exception { 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 void testSeekBackAfterReadingAhead() throws Exception { @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 void testPrepareNewSource() throws Exception { 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 void testPrepareNewSource() throws Exception { @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 void testReprepareAfterError() throws Exception { @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 void testDynamicTimelineChange() throws Exception { 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 void run(SimpleExoPlayer player) { .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 @@ private static TestAnalyticsListener runAnalyticsTest( TestAnalyticsListener listener = new TestAnalyticsListener(); try { new ExoPlayerTestRunner.Builder() - .setMediaSources(mediaSource) + .setMediaSource(mediaSource) .setRenderersFactory(renderersFactory) .setAnalyticsListener(listener) .setActionSchedule(actionSchedule) @@ -765,7 +731,7 @@ private static final class FakeVideoRenderer extends FakeRenderer { 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 @@ private static final class FakeAudioRenderer extends FakeRenderer { 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 @@ private static final class TestAnalyticsListener implements AnalyticsListener { 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 void onPlayerStateChanged( @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 09539bcb767..af6b91fa23d 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 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.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.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 static final class Seek extends Action { private final Integer windowIndex; private final long positionMs; - private final boolean catchIllegalSeekException; /** * Action calls {@link Player#seekTo(long)}. @@ -128,7 +123,6 @@ public Seek(String tag, long positionMs) { super(tag, "Seek:" + positionMs); this.windowIndex = null; this.positionMs = positionMs; - catchIllegalSeekException = false; } /** @@ -137,191 +131,24 @@ public Seek(String tag, long positionMs) { * @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 @@ protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlayWhenReady(playWhenReady); } + } /** @@ -440,28 +268,42 @@ protected void doActionImpl( } } - /** 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 PrepareSource(String tag, MediaSource mediaSource) { + this(tag, mediaSource, true, true); + } + /** @param tag A tag to use for logging. */ - public Prepare(String tag) { - super(tag, "Prepare"); + 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 @@ protected void doActionImpl( } } - /** 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 static final class SetShuffleModeEnabled extends 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 @@ protected void doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { player.setPlaybackParameters(playbackParameters); } + } /** Throws a playback exception on the playback thread. */ @@ -683,35 +505,18 @@ protected void doActionImpl( /** 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 @@ protected void doActionAndScheduleNextImpl( @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 @@ protected void doActionImpl( } } - /** 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 b977956a92d..c77e88c981f 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.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.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 Builder seek(long positionMs) { * @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 Builder setVideoSurface() { } /** - * Schedules a set media items 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)); - } - - /** - * Schedules a set media items 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. + * Schedules a new source preparation 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)); + public Builder prepareSource(MediaSource mediaSource) { + return apply(new PrepareSource(tag, mediaSource)); } /** - * Schedules a remove media item action to be executed. + * Schedules a new source preparation 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 Builder setRepeatMode(@Player.RepeatMode int repeatMode) { 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 Builder sendMessage( * @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 b00ad287bb7..5f01d7724b2 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 final void onStart(HostActivity host, Surface surface) { 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 f5d322d0cad..59afaf7dcac 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.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 static final class Builder { 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 static final class Builder { 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 Builder setTimeline(Timeline timeline) { /** * 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 Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { * 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 Builder setClock(Clock clock) { /** * 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 ExoPlayerTestRunner build(Context context) { 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 ExoPlayerTestRunner build(Context context) { return new ExoPlayerTestRunner( context, clock, - mediaSources, + mediaSource, renderersFactory, trackSelector, loadControl, @@ -338,7 +332,7 @@ public ExoPlayerTestRunner build(Context context) { 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 ExoPlayerTestRunner build(Context context) { 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 ExoPlayerTestRunner build(Context context) { private ExoPlayerTestRunner( Context context, Clock clock, - List mediaSources, + MediaSource mediaSource, RenderersFactory renderersFactory, DefaultTrackSelector trackSelector, LoadControl loadControl, @@ -376,7 +369,7 @@ private ExoPlayerTestRunner( 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 @@ private ExoPlayerTestRunner( 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 ExoPlayerTestRunner start(boolean doPrepare) { 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 ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) /** * 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 void assertTimelineChangeReasonsEqual(Integer... reasons) { 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 @@ private void handleException(Exception exception) { @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 void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra @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 TestSimpleExoPlayer( 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 a826e73e16d..18eaec2cd7a 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.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 void retry() { throw new UnsupportedOperationException(); } - @Override - public void prepare() { - throw new UnsupportedOperationException(); - } - @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -113,77 +106,6 @@ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean rese 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 int getRepeatMode() { 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 52c12f78b2e..facfa0d7e4b 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.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 static ExtractorInput getExtractorInputFromPosition( } 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 dd9ce38d35c..3348f7cffb2 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 fd949db6a25..0e004b0df8c 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 @@ private void updatePlayPauseButton() { 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 @@ private void updateProgress() { // 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 @@ private void updateProgress() { } 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 boolean dispatchMediaKeyEvent(KeyEvent event) { 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 void onPlayerStateChanged(boolean playWhenReady, @Player.State int playba 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 e4fcb37af3f..a5823712f9d 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 void onBitmap(final Bitmap bitmap) { 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 final void setPlayer(@Nullable Player player) { } this.player = player; if (player != null) { - wasPlayWhenReady = player.getPlayWhenReady(); - lastPlaybackState = player.getPlaybackState(); player.addListener(playerListener); startOrUpdateNotification(); } @@ -1070,10 +1066,9 @@ protected NotificationCompat.Builder createNotification( // 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 @@ protected List getActions(Player player) { 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 @@ protected int[] getActionIndicesForCompactView(List actionNames, Player 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 @@ private void seekTo(Player player, int windowIndex, long positionMs) { 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 @@ private class PlayerListener implements Player.EventListener { @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 6c90a3660df..d7324249e0b 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 final void removeListener(DefaultDrmSessionEventListener eventListener) { 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 03c199bda09..d30b782b30b 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 void onDrmKeysRemoved() { 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 fb64a7d13b4..052cd7d0a2a 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 @@ protected DrmSessionManager buildDrmSessionManager( 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 14219b8dfd1..211fc13ea5f 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 @@ private static String getDiagnosticInfoV21(Throwable cause) { 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 d7324249e0b..7104e4bad7b 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 @@ private MissingSchemeDataException(UUID uuid) { 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 @@ private MissingSchemeDataException(UUID uuid) { 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 @@ private MissingSchemeDataException(UUID uuid) { /** * @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 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 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 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 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 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 void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) { // 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 boolean canAcquireSession(DrmInitData drmInitData) { @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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa @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 @@ private void maybeCreateMediaDrmHandler(Looper playbackLooper) { 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 d30b782b30b..79dc743bc97 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 synchronized void releaseLicense(byte[] offlineLicenseKeySetId) 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 synchronized Pair getLicenseDurationRemainingSec(byte[] offli 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 void release() { 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 3704a09da04..a3d3a7d967d 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 @@ /** 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 664a22e15ef..89685bdce6b 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 cbbf5cacbca..dd8fbee53cb 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 @@ public void setPlayWhenReady( 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 fafbd25c32f..d809dcbc882 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 @@ default void onLoadingChanged(boolean isLoading) {} */ 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 43154a4b3f0..5a57844c835 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.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 final void onPlayerStateChanged(boolean playWhenReady, @Player.State int } } + @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 656548df475..e16d92df9ea 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.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 EventTime( 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 7104e4bad7b..10a11d95a8a 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 @@ private MissingSchemeDataException(UUID uuid) { 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 DefaultDrmSessionManager( optionalKeyRequestParameters, multiSession, /* allowPlaceholderSessions= */ false, + /* flags= */ 0, new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } @@ -187,6 +189,7 @@ private 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 @@ private 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa 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 e618d7f134c..f87f411187d 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 @@ 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 @@ public interface Callback extends SequenceableLoader.Callback 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 @@ public interface Callback extends SequenceableLoader.Callback sampleQueueMappingDoneByType; + private SparseIntArray sampleQueueIndicesByType; private int primarySampleQueueType; private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; @@ -188,8 +191,8 @@ public HlsSampleStreamWrapper( 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 @@ public LoadErrorAction onLoadError( */ 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 @@ public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) 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] - : createDummyTrackOutput(id, type); - } - 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] + 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]; } - videoSampleQueueMappingDone = true; - sampleQueueTrackIds[videoSampleQueueIndex] = id; - return sampleQueues[videoSampleQueueIndex]; } else if (tracksEnded) { return createDummyTrackOutput(id, type); } @@ -866,13 +857,8 @@ public TrackOutput track(int id, int type) { 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 f87f411187d..56e913b357e 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 @@ public TrackOutput track(int id, int type) { 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 @@ public TrackOutput track(int id, int type) { 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 10a11d95a8a..92cb5d7b8bd 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 @@ private MissingSchemeDataException(UUID uuid) { 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa } } - 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa 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 @@ private void onSessionReleased(DefaultDrmSession drmSession) { 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 | 44 +++++++++---------- library/core/proguard-rules.txt | 5 +++ .../video/VideoDecoderOutputBuffer.java | 29 +++++++++++- .../VideoDecoderOutputBufferRenderer.java | 11 ++--- .../video/VideoDecoderRenderer.java | 31 ++++++------- .../video/VideoDecoderSurfaceView.java | 19 ++++---- 10 files changed, 123 insertions(+), 84 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 d4e07952937..ff5d60a74a9 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.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 void run() { 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 13f020031c3..79d1d2e66fa 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.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 @@ *

      • 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 void handleMessage(int messageType, @Nullable Object message) throws ExoP 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 void handleMessage(int messageType, @Nullable Object message) throws ExoP // 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 f6b1ddcceab..d6ab6efc8d9 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.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 @@ public VpxDecoder( 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 @@ protected VideoDecoderInputBuffer createInputBuffer() { } @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 @@ protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { @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 @@ public void release() { } /** 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 @@ private native long vpxSecureDecode( 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 7177cde12e5..1c434032d0e 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 @@ 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 303672334db..9c4b6d4acf0 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 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. +#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__) + +// 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 ab3cc5fccd3..67646be956a 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 a3d3a7d967d..299b5656b8b 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 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 d07e24d9204..c57794f4540 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 d82f5a60711..ab338a0af54 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.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 @@ 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 @@ 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 @@ public VpxRenderer() { /** * 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 @@ public void onSurfaceChanged(GL10 unused, int width, int height) { @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 @@ public void onDrawFrame(GL10 unused) { } 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 9dd24326221..f2a4c2d002e 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 VpxVideoSurfaceView(Context context, @Nullable AttributeSet attrs) { } @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 89685bdce6b..0fef76f7370 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 92cb5d7b8bd..e8a6fe65723 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.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 @@ 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 @@ private 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 9c4b6d4acf0..823f9b8cab9 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 299b5656b8b..44ab1685057 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 interface Owner { * * @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 34230db2ec0..b7ea254a6c4 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 c8755f9aee5..ba0968cbd44 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 Metadata decode(byte[] data, int size) { 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 0fef76f7370..349b9045894 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 ff5d60a74a9..3dd039118ca 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.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 void run() { .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 79d1d2e66fa..fe59031429f 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 @@ *

          *
        • 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 @@ protected boolean hasOutputSurface() { 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 c8a73fbe9a7..b235715f46c 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 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 @@ private 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 9d0692cf0e9..664801ffd4b 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 @@ public void stop(boolean reset) { @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 @@ public synchronized void sendMessage(PlayerMessage message) { } 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 @@ public synchronized void setForegroundMode(boolean foregroundMode) { .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 @@ public synchronized void setForegroundMode(boolean foregroundMode) { } public synchronized void release() { - if (released) { + if (released || !internalPlaybackThread.isAlive()) { return; } handler.sendEmptyMessage(MSG_RELEASE); @@ -991,6 +994,11 @@ private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackExcept 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 56e913b357e..b87f83c336c 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 @@ public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) @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 @@ public TrackOutput track(int id, int type) { 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 @@ public TrackOutput track(int id, int type) { 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 bd8fb213eda..b370c893dec 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 8b9f1d1ced1..6768677fa47 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 interface Callback { @Nullable private String activeAdPlayback; private boolean playWhenReady; @Player.State private int playbackState; + private boolean isSuppressed; private float playbackSpeed; /** @@ -205,7 +206,7 @@ public void onAdPlaybackStarted(EventTime eventTime, String contentSession, Stri eventTime.currentPlaybackPositionMs, eventTime.totalBufferedDurationMs); Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) - .onSuspended(contentEventTime, /* belongsToPlayback= */ true); + .onInterruptedByAd(contentEventTime); } @Override @@ -222,7 +223,7 @@ public void onSessionFinished(EventTime eventTime, String session, boolean autom 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 void onPlayerStateChanged( } } + @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 @@ private static final class PlaybackStatsTracker { 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 void onPlayerStateChanged( 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 void onFatalError(EventTime eventTime, Exception error) { fatalErrorHistory.add(Pair.create(eventTime, error)); } hasFatalError = true; - isSuspended = false; + isInterruptedByAd = false; isSeeking = false; maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); } @@ -587,16 +617,24 @@ public void onForeground(EventTime eventTime) { } /** - * 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 @@ private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlay 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 @@ private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlay } 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 @@ private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlay 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 @@ private void maybeRecordAudioFormatTime(long nowMs) { 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 @@ private static boolean isPausedState(@PlaybackState int state) { 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 c37e98776e0..ec9ddba1227 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.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 void onPlayerStateChanged( 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 void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int periodCount = eventTime.timeline.getPeriodCount(); int windowCount = eventTime.timeline.getWindowCount(); logd( - "timelineChanged [" + "timeline [" + getEventTimeString(eventTime) + ", periodCount=" + periodCount @@ -178,10 +194,10 @@ public void onTracksChanged( 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 void onAudioSessionId(EventTime eventTime, int audioSessionId) { 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 void onDecoderInitialized( public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { logd( eventTime, - "decoderInputFormatChanged", + "decoderInputFormat", getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @@ -319,7 +354,7 @@ public void onVideoSizeChanged( int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - logd(eventTime, "videoSizeChanged", width + ", " + height); + logd(eventTime, "videoSize", width + ", " + height); } @Override @@ -378,7 +413,7 @@ public void onBandwidthEstimate( @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 void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData @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 @@ private static String getTrackTypeString(int trackType) { 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 349b9045894..5743d9944c9 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 2857141f8f7..54ff7e67775 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 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj /* 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 2995df4ab42..edaa4cde29b 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 c496052f94d..ce1a58822c4 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 @@ * 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 static final class Window { 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 Window set( long windowStartTimeMs, boolean isSeekable, boolean isDynamic, + boolean isLive, long defaultPositionUs, long durationUs, int firstPeriodIndex, @@ -212,6 +219,7 @@ public Window set( 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 8727fc5ed9e..891cb351c1e 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj /* 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 c768fe49818..4af5bafddae 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 @@ 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 @@ interface Listener { private int enabledTrackCount; private long durationUs; private long length; + private boolean isLive; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -551,7 +553,7 @@ public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, 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 @@ private void maybeFinishPrepare() { } 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 80bcdcd029f..c88972da629 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 int[] getSupportedTypes() { 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 Object getTag() { protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; drmSessionManager.prepare(); - notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive); } @Override @@ -293,27 +294,32 @@ protected void releaseSourceInternal() { // 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 3bb7ada7e09..a950a95457e 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 SilenceMediaSource(long durationUs) { @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 49d67935a53..45f64cacf23 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 SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynam * @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 SinglePeriodTimeline( long durationUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this( @@ -71,6 +76,7 @@ public SinglePeriodTimeline( /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + isLive, manifest, tag); } @@ -87,6 +93,7 @@ public SinglePeriodTimeline( * 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 SinglePeriodTimeline( long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this( @@ -108,6 +116,7 @@ public SinglePeriodTimeline( windowDefaultStartPositionUs, isSeekable, isDynamic, + isLive, manifest, tag); } @@ -127,6 +136,7 @@ public SinglePeriodTimeline( * 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 SinglePeriodTimeline( long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; @@ -149,6 +160,7 @@ public SinglePeriodTimeline( this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.manifest = manifest; this.tag = tag; } @@ -182,6 +194,7 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj 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 04ee3a153c4..be939fd0183 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 @@ private SingleSampleMediaSource( 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 afcce904e93..1a0e13b6c1f 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 89acb3ec3e5..532ad61b856 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 void setUp() throws Exception { @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 void testNoClipping() throws IOException { @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 void testClippingUnseekableWindowThrows() throws IOException { @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 void testClippingStart() throws IOException { @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 void testClippingStartAndEndInitial() throws IOException { // 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 void testClippingToEndOfSourceWithDurationSetsDuration() throws IOExcepti 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 void testClippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throw // 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 void testClippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throw @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 void testClippingFromDefaultPosition() throws IOException { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -207,6 +238,7 @@ public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOExceptio /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -217,6 +249,7 @@ public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOExceptio /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -256,6 +289,7 @@ public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -266,6 +300,7 @@ public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -305,6 +340,7 @@ public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -315,6 +351,7 @@ public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOExcep /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -355,6 +392,7 @@ public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOEx /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); Timeline timeline2 = @@ -365,6 +403,7 @@ public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOEx /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, /* manifest= */ null, /* tag= */ null); @@ -480,7 +519,10 @@ private static MediaLoadData getClippingMediaSourceMediaLoadData( 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 cb21db8212b..6ff4f78fa20 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 void setUp() throws Exception { @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 void testGetPeriodPositionDynamicWindowKnownDuration() { /* 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 void setNullTag_returnsNullTag_butUsesDefaultUid() { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, /* tag= */ null); @@ -104,6 +108,7 @@ public void getWindow_setsTag() { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* isLive= */ false, /* manifest= */ null, tag); @@ -117,6 +122,7 @@ public void getIndexOfPeriod_returnsPeriod() { /* 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 ee168f04581..352131d70ae 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, - isDynamic, + /* isDynamic= */ isMovingLiveWindow(manifest), + /* isLive= */ manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, /* firstPeriodIndex= */ 0, @@ -1247,7 +1244,7 @@ public int getIndexOfPeriod(Object uid) { 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 Object getUidOfPeriod(int periodIndex) { 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 1bda50ba88a..f058ad5ba2f 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 void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + /* isLive= */ true, manifest, tag); } else /* not live */ { @@ -455,6 +456,7 @@ public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { 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 9ddb1e39326..4c053531864 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 @@ private void processManifest() { /* windowPositionInPeriodUs= */ 0, /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, - manifest.isLive, + /* isDynamic= */ manifest.isLive, + /* isLive= */ manifest.isLive, manifest, tag); } else if (manifest.isLive) { @@ -719,6 +720,7 @@ private void processManifest() { defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, manifest, tag); } else { @@ -732,6 +734,7 @@ private void processManifest() { /* 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 af672f0da30..401fcf80340 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj /* 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 baa2a767b5a..2646cbc0352 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 final boolean isCurrentWindowDynamic() { 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 d809dcbc882..5f00916892f 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 void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { */ 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 4731d5ba991..2ec73a6fd8d 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 36d3268005e..e8a0722d031 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 42050d7eb96..f0527eac0e7 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 @@ @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 c0980df4401..8824c9e8d81 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 c8d4668a6ad..91ddf725ce0 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 d481de33cea..d660d0342ab 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 b5f40884d82..f3ad83b67d3 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 5743d9944c9..a84f4ff0b7b 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 43a5ebab99a..418aa366e67 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 SimpleExoPlayer build() { private final AnalyticsCollector analyticsCollector; private final AudioFocusManager audioFocusManager; + private final WakeLockManager wakeLockManager; @Nullable private Format videoFormat; @Nullable private Format audioFormat; @@ -453,6 +454,7 @@ protected SimpleExoPlayer( ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); } audioFocusManager = new AudioFocusManager(context, componentListener); + wakeLockManager = new WakeLockManager(context); } @Override @@ -1226,6 +1228,7 @@ public void stop(boolean reset) { public void release() { verifyApplicationThread(); audioFocusManager.handleStop(); + wakeLockManager.setStayAwake(false); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -1348,6 +1351,22 @@ public long getContentBufferedPosition() { 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 void onLoadingChanged(boolean isLoading) { } } } + + @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 00000000000..58c3748f397 --- /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 bbf7476d25e..d87376feb03 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 de18d370ec2..1ad0ce6b79f 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 void decodeMetadata() throws Exception { /* 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 void decodeMetadata_handlesId3WrappedInEmsg() throws Exception { /* 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 void decodeMetadata_handlesScte35WrappedInEmsg() throws Exception { /* 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 void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception { /* 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 883ca7420e5..af4bf3ad704 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 @@ private void parseAndDiscardSamples() { } 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 | 46 +++++++++++++++---- ...cyStreamInfoTest.java => IcyInfoTest.java} | 4 +- 5 files changed, 61 insertions(+), 26 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 a84f4ff0b7b..50418ded284 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 12f65f1cdaf..13d6b485b37 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 Metadata decode(MetadataInputBuffer inputBuffer) { return decode(Util.fromUtf8Bytes(data, 0, length)); } - @Nullable @VisibleForTesting /* package */ Metadata decode(String metadata) { String name = null; @@ -63,12 +59,9 @@ public Metadata decode(MetadataInputBuffer inputBuffer) { 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 e6b915a6c80..717bb2b2e25 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.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 boolean equals(@Nullable Object obj) { 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 4602d172a66..72237d665c8 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 void decode() { @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_extraTags() { + String icyContent = + "StreamTitle='test title';StreamURL='test_url';CustomTag|withWeirdSeparator"; + IcyDecoder decoder = new IcyDecoder(); + 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(); - Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';"); + 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 void decode_emptyTitle() { @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 void decode_semiColonInTitle() { @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 void decode_quoteInTitle() { @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 2bffe171d35..2c8e6616c97 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 @@ /** 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 50418ded284..83997fe3c7a 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 e329dc722e6..38b4a1da031 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 @@ 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 FileDataSourceException(IOException cause) { super(cause); } + public FileDataSourceException(String message, IOException cause) { + super(message, cause); + } } @Nullable private RandomAccessFile file; @@ -55,8 +60,8 @@ public long open(DataSpec dataSpec) throws FileDataSourceException { 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 long open(DataSpec dataSpec) throws FileDataSourceException { 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 035e3bfad8a..c66ce826144 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 @@ protected boolean processOutputBuffer( 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 @@ protected void onProcessedOutputBuffer(long presentationTimeUs) { 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 b1a12f6bc9d..894012664c1 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.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.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 @@ interface Listener { 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 @@ public PlayerManager( 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 @@ public boolean removeItem(MediaItem item) { 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 @@ public void release() { 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 @@ private MediaSource buildMediaSource(MediaItem item) { 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 @@ private MediaSource buildMediaSource(MediaItem item) { 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 @@ private MediaSource buildMediaSource(MediaItem item) { 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 83997fe3c7a..2c46c34d211 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 d57dccd8fec..8aafb9a0e5a 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 boolean continueLoading(long positionUs) { 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 c41933b48b2..b5837051709 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 boolean continueLoading(long positionUs) { 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 344c4989ebd..17ac6c06673 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 boolean continueLoading(long positionUs) { 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 3f306c0c8a7..2e2cf9caba0 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 @@ long selectTracks( @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 cafc052f34b..afa25d6fced 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 @@ public boolean continueLoading(long positionUs) { } } + @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 4af5bafddae..7ffc0faee67 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 @@ public boolean continueLoading(long playbackPositionUs) { 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 182f0f17ccd..189c13ef0f6 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 @@ interface Callback { */ 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 a950a95457e..abaf33633ed 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 boolean continueLoading(long positionUs) { 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 8d41fbc73f0..a5d8266ef63 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 @@ public boolean continueLoading(long positionUs) { 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 6817f294b28..61e28687250 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 boolean continueLoading(long positionUs) { 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 058a5d6dd20..c035c62a7e1 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 synchronized boolean block(long timeout) throws InterruptedException { 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 4a881d53b4e..c996aadddbc 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 boolean continueLoading(long positionUs) { 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 d62474cb1b5..bb8226e172c 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 @@ public boolean continueLoading(long positionUs) { 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 7d47cc43c6b..b0b4c04b48a 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 boolean continueLoading(long 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/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index b87f83c336c..58c275664bc 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 @@ public boolean continueLoading(long positionUs) { 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 b3d950959aa..42ac82e5530 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 @@ public boolean continueLoading(long positionUs) { 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 6d141fe04ab..9c6fdc85cd7 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 boolean continueLoading(long positionUs) { 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 d524d381fa7..bcc96ef47e1 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 boolean continueLoading(long positionUs) { 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 3e52f643e7c..b308b69a59e 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 ce90f09d472..d4544e87009 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 @@ public long getNextFrameFirstSampleIndex() { * 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 08343ab38c8..658c6284481 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.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 boolean isSeekable() { @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 85f41ec6c1a..8c3ec419bfd 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 1d4daca6a07..77ce030d919 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 c3d71f07154..ab3cc5fccd3 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 77ce030d919..599be500beb 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 14fe81f6055..6218fb01d00 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 @@ private RepresentationHolder( 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 @@ private RepresentationHolder( // 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 80fecf19ccc..22ed3892ec0 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 @@ private void openNextOutputStream() throws IOException { 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 522fdc9a3f2..d9b3ff00696 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 AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) { * * @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 void close() throws IOException { 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 ccd3ab4dd6c..8417d2f7c4c 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 e4c927260d8..4148b420f1d 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 a9147b54dfa..d45ab14f52c 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 c6f00965e33..4645e3c859c 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 6f5bc5c83dd..d2a4f1cd6fb 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 @@ @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 void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IO } @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 void testParseMediaPresentationDescriptionCanParseEventStream() throws IO } @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 void testParseMediaPresentationDescriptionCanParseProgramInformation() th } @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 void testParseCea608AccessibilityChannel() { } @Test - public void testParseCea708AccessibilityChannel() { + public void parseCea708AccessibilityChannel() { assertThat( DashManifestParser.parseCea708AccessibilityChannel( buildCea708AccessibilityDescriptors("1=lang:eng"))) @@ -259,74 +337,6 @@ public void testParseCea708AccessibilityChannel() { .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 @@ private static List buildCea608AccessibilityDescriptors(String value 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 505724e846e..db60eea2695 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 RtmpDataSourceFactory(@Nullable TransferListener listener) { } @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 0b4de1b43ef..e0630c79894 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 FileDataSourceFactory(@Nullable TransferListener listener) { } @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 99f0dee2072..412f866e99e 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 static final class Factory implements DataSource.Factory { 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 Factory(DataSource.Factory upstreamFactory, Resolver resolver) { } @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 9f6fdc9d499..ab7c5be5b2b 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 final Factory setIsNetwork(boolean isNetwork) { } @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 7fc46dc363b..c7feff516ae 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 static void checkGlError() { 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 347f49e27ca..1dc56bfbc9b 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 @@ 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.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.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 @@ private void initializePlayer() { if (player == null) { Intent intent = getIntent(); - releaseMediaDrms(); mediaSource = createTopLevelMediaSource(intent); if (mediaSource == null) { return; @@ -452,38 +444,29 @@ private MediaSource createTopLevelMediaSource(Intent intent) { } 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 @@ private MediaSource createLeafMediaSource(UriSample parameters) { } 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 @@ private MediaSource createLeafMediaSource( } } - 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 @@ private DefaultDrmSessionManager buildDrmSessionManagerV18 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 @@ private void releasePlayer() { 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 f74ce8c0760..c39fffa65df 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 e8a6fe65723..731b984ab85 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 static final class Builder { 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 PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAtt 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 void setPlayer(@Nullable Player player) { 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 void setPlayer(@Nullable Player player) { 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 void setAspectRatioListener( *
        • {@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 boolean onTrackballEvent(MotionEvent ev) { /** * 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 @@ protected void onContentAspectRatioChanged( @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 d2089759f65..c01fccf54b5 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 @@ * 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 66a0f200914..20b7dc03199 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 @@ public TouchTracker(Context context, Listener listener, float pxPerDegrees) { 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 b342d5d8884..535bf320fbf 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 6e3c5988a9a..228151339ad 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 @@ /** 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 0447e8c477d..eae879c21bb 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 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 @@ private WebvttCue( 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 static class Builder { // 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 WebvttCue build() { return new WebvttCue( startTime, endTime, - text, + Assertions.checkNotNull(text), convertTextAlignment(textAlignment), line, lineType, @@ -277,6 +281,7 @@ private static int derivePositionAnchor(@TextAlignment int textAlignment) { } } + @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 da120414b40..f688bb9447b 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 @@ private void removeClearedReferences() { 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 5d3339c85a7..2833ff2d0bd 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.Util; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -73,15 +72,12 @@ public long getEventTime(int index) { @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 @@ public List getCues(long timeUs) { 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 @@ public List getCues(long timeUs) { // 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 1674e9345cb..dce8f8157f4 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 static float parsePercentage(String s) throws NumberFormatException { * 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 2bd1af01e88..91864557023 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 @@ 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.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 void setTargetVoice(String targetVoice) { * 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 void setTargetVoice(String targetVoice) { * @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 WebvttCssStyle setItalic(boolean italic) { return this; } + @Nullable public String getFontFamily() { return fontFamily; } @@ -251,6 +259,7 @@ public boolean hasBackgroundColor() { return hasBackgroundColor; } + @Nullable public Layout.Alignment getTextAlign() { return textAlign; } @@ -309,8 +318,8 @@ public void cascadeFrom(WebvttCssStyle style) { } } - 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 d8215e98473..f587d70e903 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.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 boolean parseCue( * @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 boolean parseCue( 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 boolean parseCue( 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 @@ private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByt // 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 @@ private static boolean isSupportedTag(String tagName) { } } - 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 @@ private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttC 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 @@ private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttC */ 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 @@ private StartTag(String name, int position, String voice, String[] classes) { 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 static StartTag buildStartTag(String fullTagExpression, int position) { 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 00000000000..ee429b52610 --- /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 e2713f16bfd..b1e054a0bf4 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 cc9ea2a8c77..f8120ed11bc 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 58109c16660..5b816b8c20e 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 046f90a5b27..22a4edcdae2 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 59bf5f8f870..7d6f7325489 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 358d2a2b8f4..a76fa0e5897 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 b4b0dae0024..e534f9b2ac1 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 eba20352f45..e33070e121b 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 af44e84b04c..ce88f0ef7d7 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 59bf5f8f870..7d6f7325489 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 50777dd4d63..8def61aead2 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 void invalidateMediaSessionPlaybackState() { /* 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 fb11dfae71b..9decf900a75 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 void onPositionDiscontinuity(int reason) { } @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 b1e054a0bf4..3cbb45c0280 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 712c2508469..57f00457917 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 74e919293df..85ff41c94dc 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 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.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 void onCreate(Bundle savedInstanceState) { } 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 @@ private MediaSource createTopLevelMediaSource(Intent intent) { 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 40c20c298c7..85530b993bc 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_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 @@ public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKe isLive, DrmInfo.createFromIntent(intent, extrasKeySuffix), adTagUri, - /* sphericalStereoMode= */ null); + /* sphericalStereoMode= */ null, + SubtitleInfo.createFromIntent(intent, extrasKeySuffix)); } public final Uri uri; @@ -60,6 +64,7 @@ public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKe public final DrmInfo drmInfo; public final Uri adTagUri; @Nullable public final String sphericalStereoMode; + @Nullable SubtitleInfo subtitleInfo; public UriSample( String name, @@ -68,7 +73,8 @@ public UriSample( 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 @@ public UriSample( this.drmInfo = drmInfo; this.adTagUri = adTagUri; this.sphericalStereoMode = sphericalStereoMode; + this.subtitleInfo = subtitleInfo; } @Override @@ -100,6 +107,9 @@ private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) { if (drmInfo != null) { drmInfo.addToIntent(intent, extrasKeySuffix); } + if (subtitleInfo != null) { + subtitleInfo.addToIntent(intent, extrasKeySuffix); + } } } @@ -167,6 +177,36 @@ public void addToIntent(Intent intent, String extrasKeySuffix) { } } + 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 22533fd410b..cdce29aa5ea 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 boolean onChildClick( ? 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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc !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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 efe351c70ac..e4f239df77a 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 static SimpleExoPlayer newSimpleInstance( * 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 68b3e241fe4..06aea69035e 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 @@ private MissingSchemeDataException(UUID uuid) { * 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 cd090c2c5e2..0d53b3cde08 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.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 DownloaderConstructorHelper(Cache cache, DataSource.Factory upstreamFacto * @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 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 dec035c12eb..98026c4677d 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 DefaultDataSource( 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 436cad0d645..ae115ab58cc 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 @@ * *

        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 DefaultHttpDataSource(String userAgent, @Nullable Predicate conte * @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 85ff41c94dc..2f8d0045d3b 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 @@ private MediaSource createTopLevelMediaSource(Intent intent) { 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 4efd5c5e111..d6f3999c35f 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 @@ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData 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 00000000000..ff0819d99a2 --- /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 00000000000..bb7565c0753 --- /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 00000000000..2ae99adf58d --- /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 f7b24cf2513..b619d9486fa 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 void restoreKeys(byte[] sessionId, byte[] keySetId) { 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 9846a763288..1d0e15e81c0 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.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 @@ byte[] provideKeyResponse(byte[] scope, byte[] response) */ 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 e7853e0a0b3..d0405526386 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.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 void restoreKeys(byte[] sessionId, byte[] keySetId) { mediaDrm.restoreKeys(sessionId, keySetId); } + @Override + @Nullable + @TargetApi(28) + public PersistableBundle getMetrics() { + if (Util.SDK_INT < 28) { + return null; + } + return mediaDrm.getMetrics(); + } + @Override public String getPropertyString(String propertyName) { return mediaDrm.getPropertyString(propertyName); From 880b879e8c55b1e709fd3ed6a48005737d26e75a Mon Sep 17 00:00:00 2001 From: olly 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 8def61aead2..baf5154e6c3 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 void setCustomErrorMessage( * @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 ea155323bb6..6b84662093d 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 @@ * 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 06aea69035e..f9606d591e6 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 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 7676926c4dd..0a21ddd3a35 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 TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptio : 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 07836672e56..9adb6bc7bcc 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 @@ private static final class MediaCodecListCompatV21 implements MediaCodecListComp @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 int getCodecCount() { 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 ef4bd00f20b..4517e4ee9aa 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 @@ private void ensureInitialized() throws DatabaseIOException { } } + // 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 a5d8266ef63..ca50c342b5d 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 @@ private void maybeNotifyDownstreamFormat() { @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 eae21b5b306..041435482c9 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 @@ private static final class DefaultBandwidthProvider implements BandwidthProvider @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 55c580ead23..e592c3bec35 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 5080e863450..01fa6837ea3 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 @@ // 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 d6f3999c35f..6f64112e4cf 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 @@ protected long preparePayload(ParsableByteArray packet) { } @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 baf5154e6c3..cce2ebfc285 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 @@ private static int getMediaSessionPlaybackState( 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 6f64112e4cf..cc536acb140 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 @@ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData /* 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 dbe97c02bd8..5b8d893f1ab 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 d1246a3e648..fff76c5b053 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 ec0336309ab..b4d35341615 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 1e3254a9fc1..27c29cba585 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 dbe97c02bd8..5b8d893f1ab 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 cce7bf2450e..2ecdc9784cd 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 ac36a484121..0ed2a86b9ed 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 dae0d878fa0..229e90584e6 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 c9570ab58eb..89c6d178ff4 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 7a3e7ef5ac3..7a4ba81f23e 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 afccef3e247..33798c0c308 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 3c7b13a6a89..0d6439cf6c3 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 041435482c9..3e8cdd1ca40 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 @@ 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 8ba0be26a05..e97fa878f73 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 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 b09f7f204fe..54a14abeefe 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 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 70f64d3dbe7..18a03ddc29d 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 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 3c8911adec8..4d9e08a12d0 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 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 8dd0cd16b17..c963a8f6589 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 38e4332b161..a334c5128ea 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 @@ 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 ab521fc99e5..587d8a75a75 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.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 dc3b1510ef5..6dfddb37ddc 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 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 45cdf34b6ca..2033ef3096a 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 @@ 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 052cd7d0a2a..8323d666149 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.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.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 d6b72048a12..affd762f614 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 @@ * 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 73d72446f92..f7b376d7add 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 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 f3432749d0d..94a07b9aafe 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 void logMetric(String key, int value) { 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 9edccadcab0..1aeb73a29dc 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 3813ba35334..ad37d86cb2f 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 5d68711aa71..4a326ac6468 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 2759299d63a..28cf8f138f9 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 e40d6e02d78..80239beb223 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 33798c0c308..32beddfd892 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 d77944cf23c..0e93b97f5ef 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 0d6439cf6c3..204e089bd0f 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 36ece262540..8f8d600f933 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 @@ private static String getDiagnosticInfoV21(Throwable cause) { private boolean waitingForKeys; private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; + private boolean skipMediaCodecStopOnRelease; protected DecoderCounters decoderCounters; @@ -433,6 +434,22 @@ public void experimental_setRenderTimeLimitMs(long renderTimeLimitMs) { 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 @@ protected void releaseCodec() { 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 aaa1e2ef83b..00000000000 --- 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 14e813ceb89..d962efd96b4 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 @@ /** 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 f9606d591e6..753b8a7d3a1 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 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 @@ /** 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 @@ private MissingSchemeDataException(UUID uuid) { 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 @@ private 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 getExoMediaCryptoType(DrmInitData drmInitData) { : 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 @@ private DefaultDrmSession createNewDefaultSession( return new DefaultDrmSession<>( uuid, exoMediaDrm, - /* provisioningManager= */ this, + /* provisioningManager= */ provisioningManagerImpl, /* releaseCallback= */ this::onSessionReleased, schemeDatas, mode, @@ -664,6 +633,37 @@ public void handleMessage(Message msg) { } } + 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 ca011434aca..99bc0d7abc5 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } 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 d4b7fc77cd9..829602275d2 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:layout_height="match_parent" + 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 3cbb45c0280..5d1d054fed3 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 ad37d86cb2f..e957bf3f6a7 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 adc05eb2047..79d395a8584 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 3aeff9d553a..3a13540e12f 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 static DolbyVisionConfig parse(ParsableByteArray data) { 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 894012664c1..85104e0d188 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.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 @@ private MediaSource buildMediaSource(MediaItem item) { 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 79dc743bc97..9ed2fe3f27a 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 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 08b0c52fe5e..2d74175265a 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 54a14abeefe..b80c8d38928 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 void testSniffVorbis() throws Exception { 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 void testSniffFlac() throws Exception { 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 void testSniffFailsOpusFile() throws Exception { 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 void testSniffInvalidHeader() throws Exception { 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 @@ private boolean sniff(byte[] data) throws InterruptedException, IOException { .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 3a3067853e7..452f20e9575 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 void setUp() throws Exception { 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 e28277d945b..fda57cbce41 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 @@ private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentI } 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 73780f56f3b..a1c4d2b59d9 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 void setUp() throws Exception { 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 39be9fbcd84..5908c7db202 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 static File createCacheSpanFile( 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 fc229d9dc6a..a8dbfe3b426 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 void testGetCachedLength() throws Exception { @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 5eec84408b4..264b5d39e1a 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 void setUp() throws Exception { 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 1527c5a5447..fd295ea18d2 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 void setUp() throws IOException { 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 cf363f6266a..b1ab3f94bb6 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 Builder throwPlaybackException(ExoPlaybackException exception) { 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 a933121bc5e..1ca4f1fb187 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 interface ExtractorFactory { 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 @@ private static FakeExtractorOutput assertOutput( .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 facfa0d7e4b..66ea480cc39 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.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 static byte[] readToEnd(DataSource dataSource) throws IOException { 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 static byte[] readExactly(DataSource dataSource, int length) throws IOExc 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 static byte[] createByteArray(int... intArray) { 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 static byte[] joinByteArrays(byte[]... byteArrays) { 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 | 49 ++++-- .../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, 302 insertions(+), 46 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 2f8d0045d3b..2de117e9d72 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 @@ private void initializePlayer() { 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 7c8a454191f..99089a2afcf 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,25 +331,50 @@ public ExoPlayer build() { */ void retry(); + /** Prepares the player. */ + void prepare(); + /** - * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code - * prepare(mediaSource, true, true)}. + * @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); + /** - * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback - * position the default position in the first {@link Timeline.Window}. + * Sets the specified {@link MediaSource}. * - * @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). + *

        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 prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); + 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 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 dd8fbee53cb..97658d29066 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 @@ 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 @@ public void retry() { } @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 @@ public Timeline getCurrentTimeline() { } } + /* 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 78d6f1d9d8f..c0a45249e6f 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 ExoPlaybackException getPlaybackError() { } @Override + @SuppressWarnings("deprecation") public void retry() { verifyApplicationThread(); if (mediaSource != null @@ -1149,23 +1150,38 @@ public void retry() { } @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 void setHandleWakeLock(boolean handleWakeLock) { // 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 79103bf0be4..7146e2e4052 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.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 void testSendMessagesFromStartPositionOnlyOnce() throws Exception { AtomicInteger counter = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessagesFromStartPositionOnlyOnce") + .waitForTimelineChanged() .pause() .sendMessage( (messageType, payload) -> { @@ -2860,6 +2865,155 @@ public void onPlaybackSuppressionReasonChanged( 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 d64a44ac046..bf3cc90a783 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 ExoPlayerTestRunner start(boolean doPrepare) { 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 18eaec2cd7a..47f34712b96 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 void retry() { throw new UnsupportedOperationException(); } + @Override + public void prepare() { + throw new UnsupportedOperationException(); + } + @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -106,6 +111,16 @@ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean rese 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 29ef1faa80e..73bb49ed401 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 00000000000..351c9d57800 --- /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 545b8f5155a..c1ab78a9bc6 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.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 cedc6f911da..8769a84d957 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 00000000000..cc551db8ac0 --- /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 957c3ba2099..b9ecaf174ce 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 static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) { * @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 a3770afc788..025fdfd209a 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.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 void setUp() { } } + @Test public void testGetSeekMap_returnsSeekMapWithCorrectDuration() throws IOException, FlacDecoderException, InterruptedException { byte[] data = @@ -63,6 +65,7 @@ public void testGetSeekMap_returnsSeekMapWithCorrectDuration() 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 3beb4d0103f..a64a52b4117 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.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 void setUp() throws Exception { positionHolder = new PositionHolder(); } + @Test public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -87,6 +89,7 @@ public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap() assertThat(seekMap.isSeekable()).isTrue(); } + @Test public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -103,6 +106,7 @@ public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrect trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -120,6 +124,7 @@ public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -139,6 +144,7 @@ public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() trackOutput, targetSeekTimeUs, extractedFrameIndex); } + @Test public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() throws IOException, InterruptedException { FlacExtractor extractor = new FlacExtractor(); @@ -158,6 +164,7 @@ public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() 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 97f152cea47..c8033e04d32 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.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 void setUp() { } } + @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 0ddb93dbe64..b920560f3a1 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 5e020175e76..60f1d32a790 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 @@ public FlacFrameDecodeException(String message, int errorCode) { @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 4e7422b291e..1efdeac84d8 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 DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyl } // 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 59fb7b48353..fb5d41c0dea 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 boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept if (input.getPosition() == 0) { id3Metadata = peekId3Data(input); } - return peekFlacSignature(input); + return peekFlacStreamMarker(input); } @Override @@ -255,15 +252,15 @@ private int handlePendingSeek( } /** - * 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 71359322b01..87060e8d61f 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 820b9eed10f..b12f4dbf9df 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 c2d58347eb7..613023e86c2 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 8c1115f1ecc..79f369751c4 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 d8903fcade8..3a3ba57572f 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 100fdd1eaf0..a07fcaa0a20 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 6c3cd731b3d..c4d13dd2e6c 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 decf9c6af33..2f389909e7a 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 2c814294af0..9c5862b4833 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 int maxDecodedFrameSize() { /** 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 5b8d893f1ab..ccd09181a81 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 fff76c5b053..0f36b2b32e7 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 b4d35341615..b3ff58e707c 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 27c29cba585..ea2eff8b040 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 5b8d893f1ab..ccd09181a81 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 2ecdc9784cd..0972d17f2f9 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 0ed2a86b9ed..e33b81c90fd 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 229e90584e6..b8b7d853930 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 89c6d178ff4..c8660175487 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 7a4ba81f23e..735d97eed18 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 b2aa0b7b163..ae081b12486 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 void setUp() { 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 void setUp() { } @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 void testSetPlayWhenReady_masksLocalState() { // 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 void testSetPlayWhenReadyMasking_updatesUponResultChange() { 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 void testPlayWhenReady_changesOnStatusUpdates() { 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 cbece8a59c9..76768804df6 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.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 @@ 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 @@ 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 @@ public void draw( 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 @@ private void setupTextLayout() { 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 @@ private void setupTextLayout() { 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 @@ private void drawLayout(Canvas canvas, boolean isTextCue) { if (isTextCue) { drawTextLayout(canvas); } else { + Assertions.checkNotNull(bitmapRect); + Assertions.checkNotNull(cueBitmap); drawBitmapLayout(canvas); } } @@ -438,8 +449,9 @@ private void drawTextLayout(Canvas canvas) { 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 @@ private void drawBitmapLayout(Canvas canvas) { * 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 5d1d054fed3..6f786761798 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 e957bf3f6a7..decb25c6660 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 79d395a8584..249ef7e44eb 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 bc21fb2bf3b..6e10171f08f 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 @@ 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 TrackSelectionParameters build() { @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 cac29daf5bb..faa015fd67b 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 @@ private void parseDialogueLine(String dialogueLine, List cues, LongArray cu 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 7146e2e4052..0871d8efc3c 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.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 void run(SimpleExoPlayer player) { 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 void run(SimpleExoPlayer player) { }); } + 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 7936f9b51cf..b65accdf3f8 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 @@ protected void doActionImpl( } - /** 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 @@ protected void doActionImpl( } } + /** + * 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 static final class WaitForPlaybackState extends 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 b1ab3f94bb6..f6ab4b9924e 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.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.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 Builder sendMessage( 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 Builder waitForPositionDiscontinuity() { 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 6f786761798..06f925a68dd 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 643b1203dc6..ff00c5b0d47 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 @@ protected static String parseEac3SupplementalProperties(List supplem 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 cc536acb140..cef274b9030 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 @@ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData 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 @@ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData 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 ccd09181a81..365040c46c7 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 0f36b2b32e7..ff020b32fd4 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 b3ff58e707c..88deeaebd31 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 ea2eff8b040..2eb7be2454b 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 ccd09181a81..365040c46c7 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 0972d17f2f9..c07b2f3844c 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 e33b81c90fd..a7fce3c9015 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 b8b7d853930..d05d36bd1ef 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 c8660175487..376cb684996 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 735d97eed18..44a93a60371 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 101a1f74d97..bc590c9d4c9 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 void consume(ParsableByteArray data, @Flags int flags) { 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 ec022eaac33..50ba7ba1d9d 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 static File createTempFile(Context context, String prefix) throws IOExcep } /** - * 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 static File createTempFile(Context context, String prefix) throws IOExcep * @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 static int crc(byte[] bytes, int start, int end, int initialValue) { 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 @@ private static HashMap createIso3ToIso2Map() { }; /** - * 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 b54bb0b160f..dd86655d8a5 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 void testEscapeUnescapeFileName() { } } + @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 bc590c9d4c9..101a1f74d97 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 void consume(ParsableByteArray data, @Flags int flags) { 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 50ba7ba1d9d..ec022eaac33 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 static File createTempFile(Context context, String prefix) throws IOExcep } /** - * 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 static File createTempFile(Context context, String prefix) throws IOExcep * @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 static int crc32(byte[] bytes, int start, int end, int initialValue) { 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 @@ private static HashMap createIso3ToIso2Map() { }; /** - * 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 dd86655d8a5..b54bb0b160f 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 void testEscapeUnescapeFileName() { } } - @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 21d2bee056f..e362697179a 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 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFo @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 9decf900a75..569fc934565 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 @@ private void stopNotification(boolean dismissedByUser) { * 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 1377f74f2a8..03d9b7413db 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 101a1f74d97..bc590c9d4c9 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 void consume(ParsableByteArray data, @Flags int flags) { 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 ec022eaac33..a6e49fd6459 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 static File createTempFile(Context context, String prefix) throws IOExcep } /** - * 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 static File createTempFile(Context context, String prefix) throws IOExcep * @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 static int crc(byte[] bytes, int start, int end, int initialValue) { 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 @@ private static HashMap createIso3ToIso2Map() { }; /** - * 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 b54bb0b160f..172fdf31ad0 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 void testEscapeUnescapeFileName() { } } + @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 56ba01b967d..d4849968bf2 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 @@ private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, 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 @@ private static void setSampleDuration(byte[] subripSampleData, long durationUs, 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 06f925a68dd..8b8f69f6a6b 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 d4849968bf2..517c087e186 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 @@ private void commitSampleToOutput(Track track, long timeUs) { 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 @@ private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, // 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 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 void setSubtitleSampleDuration( - byte[] subtitleSampleData, - long durationUs, - String timecodeFormat, - int endTimecodeOffset, - long lastTimecodeValueScalingFactor, - byte[] emptyTimecode) { + 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 faa015fd67b..2e78b433bd9 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 @@ private void parseDialogueLine(String dialogueLine, List cues, LongArray cu 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 @@ private void parseDialogueLine(String dialogueLine, List cues, LongArray cu .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 8d1b743a6dc..20b7efe50a7 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { } // Read and parse the timing line. - boolean haveEndTimecode = false; currentLine = subripData.readLine(); if (currentLine == null) { Log.w(TAG, "Unexpected end"); @@ -99,11 +98,8 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { } } 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 10ebfc31094..380d330a86a 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 b949179533a..00000000000 --- 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 df2c44b956a..00000000000 --- 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 2c6fe69b6f8..cd25ffca3b2 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 70959628018..3c48aa61dd2 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 void testDecodeInvalidTimecodes() throws IOException { 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 774e8d98b94..9f66f65a569 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 void testDecodeTypicalUnexpectedEnd() throws IOException { 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 293d1e7510a..68ab2a7a47c 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 @@ */ 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 @@ long getBlacklistDurationMsFor( */ 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 753b8a7d3a1..15f87318f08 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 boolean canAcquireSession(DrmInitData drmInitData) { @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 46f94584084..1a9e01441f6 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 @@ 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 @@ default void release() { * 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 28aa5d86e03..891f36f47c6 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.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 @@ private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { 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 944a9987815..df6ccc1f021 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 void testAllowPlaceholderSessionPopulatesDrmSession() { 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 db5483be1da..4c25c180f43 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 @@ private void handleSourceInfoRefreshEndedPlayback() { * @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 @@ private Pair resolveSeekPosition( } 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 0871d8efc3c..8bd6b1ba098 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 void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNu 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 void testSeekTo_windowIndexIsReset() throws Exception { 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 8b8f69f6a6b..b4485accbdf 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 1d0e15e81c0..b6ee644842b 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 @@ /** * 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 @@ byte[] provideKeyResponse(byte[] scope, byte[] response) 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 d0405526386..8ac92b093c2 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 6c323deca4a..3cdab8baf16 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 @@ protected final DrmSession getUpdatedSourceDrmSess 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 32516e7dcd9..76abfd6e0fc 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 @@ private void releaseDecoder() { } 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 d962efd96b4..25a08c90588 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 @@ public byte[] getOfflineLicenseKeySetId() { } @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 @@ public void acquireReference() { } @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 15f87318f08..a65f45309ab 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 DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackT sessions.add(placeholderDrmSession); this.placeholderDrmSession = placeholderDrmSession; } - placeholderDrmSession.acquireReference(); + placeholderDrmSession.acquire(); return placeholderDrmSession; } @@ -521,7 +521,7 @@ public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa } 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 722ab946f0f..13e29e141a6 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 @@ 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 DrmSessionException(Throwable cause) { 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 1a9e01441f6..c92d68ed173 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 @@ default void release() { 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 @@ default DrmSession acquirePlaceholderSession(Looper playbackLooper, int track } /** - * 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 d40cf609069..aa15c829009 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 byte[] getOfflineLicenseKeySetId() { } @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 9ed2fe3f27a..31211d7b2a5 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 synchronized Pair getLicenseDurationRemainingSec(byte[] offli 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 @@ private byte[] blockingKeyRequest( 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 8f8d600f933..80d2625a3ef 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 @@ private void resetOutputBuffer() { } 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 891f36f47c6..0cc576a145b 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 @@ public void maybeThrowError() throws 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 @@ private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { 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 c7dc0568239..cd3823b3425 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 @@ protected final void setOutputBufferRenderer( // 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 fa0caeb92dd..a29851fefce 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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { * * @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 @@ interface VideoComponent { */ 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 c0a45249e6f..6bea81cd493 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 SimpleExoPlayer build() { @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 void clearVideoSurface() { } @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 void setVideoSurface(@Nullable Surface surface) { } @Override - public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); removeSurfaceCallbacks(); this.surfaceHolder = surfaceHolder; @@ -559,7 +560,7 @@ public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { } @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 void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { } @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 void setVideoTextureView(TextureView textureView) { } @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 void setVideoDecoderOutputBufferRenderer( @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 @@ private void setVideoSurfaceInternal(@Nullable Surface surface, boolean ownsSurf 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 6bea81cd493..f1d01a114fe 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 void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { @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 void clearVideoSurface(@Nullable Surface surface) { 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 void setVideoSurface(@Nullable Surface surface) { 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 void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { 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 void clearVideoTextureView(@Nullable TextureView textureView) { public void setVideoDecoderOutputBufferRenderer( @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { verifyApplicationThread(); - setVideoSurface(null); + if (videoDecoderOutputBufferRenderer != null) { + clearVideoSurface(); + } setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer); } @@ -630,7 +643,7 @@ public void clearVideoDecoderOutputBufferRenderer( verifyApplicationThread(); if (videoDecoderOutputBufferRenderer != null && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) { - setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); + clearVideoDecoderOutputBufferRenderer(); } } @@ -1729,7 +1742,7 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig @Override public void surfaceDestroyed(SurfaceHolder holder) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -1737,7 +1750,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { @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 void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width @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 cd3823b3425..86181664bac 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 @@ protected final void setOutputSurface(@Nullable Surface surface) { // 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 @@ protected final void setOutputBufferRenderer( // 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 351c9d57800..24ea65893e3 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 @@ public final Timeline addMediaSources( 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 b4485accbdf..afdae31cf33 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 31211d7b2a5..93a7585f899 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.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 static OfflineLicenseHelper newWidevineInstance( * @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 static OfflineLicenseHelper newWidevineInstance( * 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 void onDrmKeysRemoved() { 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 c371389483d..e3dd679b92d 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 void setUp() throws Exception { .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 afdae31cf33..969b549083f 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 969003101fe..51556859992 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.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 int compareTo(Segment other) { } 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 final void download(@Nullable ProgressListener progressListener) 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 final void download(@Nullable ProgressListener progressListener) contentLength = C.LENGTH_UNSET; } } - Collections.sort(segments); // Download the segments. @Nullable ProgressNotifier progressNotifier = null; @@ -232,6 +236,44 @@ protected static DataSpec getCompressibleDataSpec(Uri uri) { /* 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 969b549083f..a898646b30c 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 80d2625a3ef..54e0904dab4 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 @@ private static String getDiagnosticInfoV21(Throwable cause) { private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; private boolean skipMediaCodecStopOnRelease; + private boolean pendingOutputEndOfStream; protected DecoderCounters decoderCounters; @@ -603,6 +604,7 @@ protected void onEnabled(boolean joining) throws ExoPlaybackException { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; + pendingOutputEndOfStream = false; flushOrReinitializeCodec(); formatQueue.clear(); } @@ -686,6 +688,10 @@ protected void onStopped() { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (pendingOutputEndOfStream) { + pendingOutputEndOfStream = false; + processEndOfStream(); + } try { if (outputStreamEnded) { renderToEndOfStream(); @@ -1693,6 +1699,14 @@ private void processEndOfStream() throws ExoPlaybackException { } } + /** + * 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 871ad1689c6..c04230e2b02 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 @@ protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { @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 @@ protected void onProcessedTunneledBuffer(long presentationTimeUs) { 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 void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nano // 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 e06a3278f0a..bf05424b7f9 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 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType 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 @@ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType 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 f4f143f3b01..3372f239710 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 static Pair parseAacAudioSpecificConfig(byte[] audioSpe } /** - * 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 static Pair parseAacAudioSpecificConfig(byte[] audioSpe * @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 static Pair parseAacAudioSpecificConfig(ParsableBitArra * 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 static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChanne } 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 static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sample 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 00000000000..a880e822505 --- /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 32beddfd892..e9cb6d5a18e 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 ac90d64c1e9..f51fc509cc1 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 07696c1c26d..df880f7f418 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 4fe2fae3282..6ced528631b 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 509dd22a282..b6bf139963f 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 a898646b30c..5e4c9b78aa6 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 51556859992..969003101fe 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.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 int compareTo(Segment other) { } 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 final void download(@Nullable ProgressListener progressListener) 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 final void download(@Nullable ProgressListener progressListener) contentLength = C.LENGTH_UNSET; } } + Collections.sort(segments); // Download the segments. @Nullable ProgressNotifier progressNotifier = null; @@ -236,44 +232,6 @@ protected static DataSpec getCompressibleDataSpec(Uri uri) { /* 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 24ea65893e3..351c9d57800 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 @@ public final Timeline addMediaSources( 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 68ab2a7a47c..293d1e7510a 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 @@ */ 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 LoadErrorInfo(int dataType, long loadDurationMs, IOException exception, i 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 @@ default long getBlacklistDurationMsFor(LoadErrorInfo loadErrorInfo) { */ 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 351c9d57800..00000000000 --- 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 73bb49ed401..29ef1faa80e 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 c1ab78a9bc6..545b8f5155a 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.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 8769a84d957..cedc6f911da 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 cc551db8ac0..00000000000 --- 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 | 49 +--- .../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, 46 insertions(+), 365 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 2de117e9d72..2f8d0045d3b 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 @@ private void initializePlayer() { 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 99089a2afcf..7c8a454191f 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,50 +331,25 @@ public ExoPlayer build() { */ 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 - 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. + * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback + * position the default position in the first {@link Timeline.Window}. * - *

        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. + * @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 setMediaItem(MediaSource mediaItem, long startPositionMs); + 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/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 97658d29066..dd8fbee53cb 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 @@ 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 @@ public void retry() { } @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); - } - - @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); + 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 setMediaItem(MediaSource mediaItem) { - mediaSource = mediaItem; - } @Override public void setPlayWhenReady(boolean playWhenReady) { @@ -610,29 +606,6 @@ public Timeline getCurrentTimeline() { } } - /* 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 f1d01a114fe..729dd150ae1 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 ExoPlaybackException getPlaybackError() { } @Override - @SuppressWarnings("deprecation") public void retry() { verifyApplicationThread(); if (mediaSource != null @@ -1172,38 +1171,23 @@ public void retry() { } @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 void setHandleWakeLock(boolean handleWakeLock) { // 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 8bd6b1ba098..8ec8cca06c4 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.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 void testSendMessagesFromStartPositionOnlyOnce() throws Exception { AtomicInteger counter = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessagesFromStartPositionOnlyOnce") - .waitForTimelineChanged() .pause() .sendMessage( (messageType, payload) -> { @@ -2931,218 +2926,6 @@ public void onPlaybackSuppressionReasonChanged( 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 bf3cc90a783..d64a44ac046 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 ExoPlayerTestRunner start(boolean doPrepare) { 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 47f34712b96..18eaec2cd7a 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 void retry() { throw new UnsupportedOperationException(); } - @Override - public void prepare() { - throw new UnsupportedOperationException(); - } - @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); @@ -111,16 +106,6 @@ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean rese 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 a860249478a..ce1a58822c4 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 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 long getPositionInFirstPeriodUs() { 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 long getAdResumePositionUs() { 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 545b8f5155a..8dfea1e5116 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 ConcatenatingMediaSource( addMediaSources(Arrays.asList(mediaSources)); } - @Override - public synchronized Timeline getInitialTimeline() { - ShuffleOrder shuffleOrder = - this.shuffleOrder.getLength() != mediaSourcesPublic.size() - ? this.shuffleOrder - .cloneAndClear() - .cloneAndInsert( - /* insertionIndex= */ 0, /* insertionCount= */ mediaSourcesPublic.size()) - : this.shuffleOrder; - return new ConcatenatedTimeline(mediaSourcesPublic, shuffleOrder, isAtomic); - } - - @Override - public boolean isSingleWindow() { - return false; - } - /** * Appends a {@link MediaSource} to the playlist. * 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 cedc6f911da..ac23e2a8317 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 @@ -35,7 +35,7 @@ */ public final class LoopingMediaSource 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 LoopingMediaSource(MediaSource childSource) { */ 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 LoopingMediaSource(MediaSource childSource, int loopCount) { @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 031d50e7d25..891cb351c1e 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 void releaseSourceInternal() { } @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 @@ protected synchronized void onChildSourceInfoRefreshed( 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 @@ protected synchronized void onChildSourceInfoRefreshed( maskingPeriod.createPeriod(idInSource); } } - hasRealTimeline = true; isPrepared = true; refreshSourceInfo(this.timeline); } @@ -208,15 +193,13 @@ protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId } 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 @@ private static final class MaskingTimeline extends ForwardingTimeline { 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 static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag * 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj @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 Period getPeriod(int periodIndex, Period period, boolean setIds) { @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 int getPeriodCount() { @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 f6dd4d79a4c..5ee980d01f7 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 int hashCode() { */ 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 5110ad411c7..d6e65cb34d5 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 void testMultiPeriodTimeline() { 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 c8c71900077..401fcf80340 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 static final class TimelineWindowDefinition { 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 TimelineWindowDefinition( 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 Window getWindow(int windowIndex, Window window, long defaultPositionProj /* 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 cce2ebfc285..84d5fea0c70 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.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 void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) * 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 a6e49fd6459..23447acddf4 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 static long subtractWithOverflowDefault(long x, long y, long overflowResu 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 static long subtractWithOverflowDefault(long x, long y, long overflowResu * @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 31c663c6eb2..4e7ad65642e 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 LibvpxVideoRenderer( eventListener, maxDroppedFramesToNotify, /* drmSessionManager= */ null, - /* playClearSamplesWithoutKeys= */ false, - /* disableLoopFilter= */ false); + /* playClearSamplesWithoutKeys= */ false); } /** @@ -121,7 +119,6 @@ public LibvpxVideoRenderer( * 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 LibvpxVideoRenderer( @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, - boolean disableLoopFilter) { + boolean playClearSamplesWithoutKeys) { this( allowedJoiningTimeMs, eventHandler, @@ -138,7 +134,6 @@ public LibvpxVideoRenderer( maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys, - disableLoopFilter, /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), /* numInputBuffers= */ 4, @@ -160,7 +155,6 @@ public LibvpxVideoRenderer( * 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 LibvpxVideoRenderer( int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads, int numInputBuffers, @@ -185,7 +178,6 @@ public LibvpxVideoRenderer( maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys); - this.disableLoopFilter = disableLoopFilter; this.enableRowMultiThreadMode = enableRowMultiThreadMode; this.threads = threads; this.numInputBuffers = numInputBuffers; @@ -225,7 +217,6 @@ protected int supportsFormatInternal( 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 1af36447442..b4535a3e9c8 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 @@ * @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 @@ public VpxDecoder( int numOutputBuffers, int initialInputBufferSize, @Nullable ExoMediaCrypto exoMediaCrypto, - boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { @@ -77,7 +75,7 @@ public VpxDecoder( 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 601ddf9b9c6..64517feec96 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 boolean isVideoSizeAndRateSupportedV21(int width, int height, double fram 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 @@ private static Point alignVideoSizeV21(VideoCapabilities capabilities, int width 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 e618fcad75d..cf8056f5a95 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 static synchronized boolean isCacheFolderLocked(File cacheFolder) { 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 void run() { * @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 @@ private static long parseUid(String fileName) { } 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 fa5b60e66ef..297f26bb71a 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 @@ default void onDrmSessionAcquired() {} * * @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 14bafdaf4b8..fbb3a86221c 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 ee731cbc09f..060027fee7e 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 void releasePeriod(MediaPeriod mediaPeriod) { } @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 ab8d86cc559..9e6f4f9cf11 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 MediaLoadData( * @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 MediaLoadData( * @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 MediaLoadData( * 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 @@ void onLoadStarted( * 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 @@ void onLoadCompleted( * 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 @@ void onLoadCanceled( * @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 @@ void onLoadError( * @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 @@ void onLoadError( * @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 @@ void onUpstreamDiscarded( * @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 be939fd0183..db1414942f4 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 @@ protected void releaseSourceInternal() { */ @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 532ad61b856..b2dc6667905 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 @@ protected FakeMediaPeriod createFakeMediaPeriod( () -> 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 5e4c9b78aa6..6451fada36b 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 517c087e186..69bdb2cd464 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 void initializeOutput(ExtractorOutput output, int trackId) throws ParserE 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 void reset() { } /** 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 @@ private byte[] getHdrStaticInfo() { } 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 2e64d9f4219..c585c79b76b 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.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 int getSelectionReason() { 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 bd99403b070..ad1a6ef1f2b 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.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 Definition(TrackGroup group, int[] tracks, int reason, @Nullable Object d 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 @@ default TrackSelection createTrackSelection( */ 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 @@ default void updateSelectedTrack( * 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 5ee3e9561e6..42fc40e72dd 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.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 int getSelectionReason() { 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 a65f45309ab..e1c7793f8ce 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 static final class Builder { 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 static final class Builder { *

      • {@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 Builder() { uuid = C.WIDEVINE_UUID; exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + useDrmSessionsForClearContentTrackTypes = new int[0]; } /** @@ -127,14 +128,27 @@ public Builder setMultiSession(boolean multiSession) { } /** - * 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 DefaultDrmSessionManager build(MediaDrmCallback mediaDrmC mediaDrmCallback, keyRequestParameters, multiSession, - preferSecureDecoders, + useDrmSessionsForClearContentTrackTypes, flags, loadErrorHandlingPolicy); } @@ -226,7 +240,7 @@ private MissingSchemeDataException(UUID uuid) { @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 DefaultDrmSessionManager( callback, optionalKeyRequestParameters, multiSession, - /* preferSecureDecoders= */ false, + /* useDrmSessionsForClearContentTrackTypes= */ new int[0], /* flags= */ 0, new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } @@ -333,7 +347,7 @@ private DefaultDrmSessionManager( MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters, boolean multiSession, - boolean preferSecureDecoders, + int[] useDrmSessionsForClearContentTrackTypes, @Flags int flags, LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkNotNull(uuid); @@ -344,7 +358,7 @@ private DefaultDrmSessionManager( 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 final void removeListener(DefaultDrmSessionEventListener eventListener) { /** * 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 boolean canAcquireSession(DrmInitData drmInitData) { @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 25a08c90588..0c36bc7d0f1 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 @@ public interface ReleaseCallback { 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 @@ public interface ReleaseCallback { * @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 @@ public DefaultDrmSession( @DefaultDrmSessionManager.Mode int mode, boolean isPlaceholderSession, @Nullable byte[] offlineLicenseKeySetId, - @Nullable HashMap optionalKeyRequestParameters, + HashMap keyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, EventDispatcher eventDispatcher, @@ -177,7 +177,7 @@ public DefaultDrmSession( } 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 @@ private long getLicenseDurationRemainingSec() { 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 e1c7793f8ce..f8e854e51da 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 Builder() { } /** - * 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 @@ private MissingSchemeDataException(UUID uuid) { } /** - * 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 @@ private MissingSchemeDataException(UUID uuid) { 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 @@ private MissingSchemeDataException(UUID uuid) { * @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 DefaultDrmSessionManager( 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 DefaultDrmSessionManager( * @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 DefaultDrmSessionManager( 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 DefaultDrmSessionManager( * @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 DefaultDrmSessionManager( 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 @@ private DefaultDrmSessionManager( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters, + HashMap keyRequestParameters, boolean multiSession, int[] useDrmSessionsForClearContentTrackTypes, @Flags int flags, @@ -355,7 +359,7 @@ private DefaultDrmSessionManager( 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 @@ private DefaultDrmSession createNewDefaultSession( 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 6451fada36b..a9fbe01e6e9 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 0c0647595ad..d17b6ebb535 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.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 LibopusAudioRenderer( * 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 4e7ad65642e..7fcb89dc126 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.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 LibvpxVideoRenderer( * 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 LibvpxVideoRenderer( /* 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 LibvpxVideoRenderer( * @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 e362697179a..873219d31d9 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.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 MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSel * 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 MediaCodecAudioRenderer( * 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 MediaCodecAudioRenderer( * @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 MediaCodecAudioRenderer( * 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 MediaCodecAudioRenderer( * 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 MediaCodecAudioRenderer( 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 MediaCodecAudioRenderer( * 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 c04230e2b02..38ac80bf263 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.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 MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSel * @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 MediaCodecVideoRenderer( * @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 MediaCodecVideoRenderer( 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 MediaCodecVideoRenderer( * @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 a9fbe01e6e9..ac749316715 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 873219d31d9..a6a8b034482 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 @@ protected boolean canKeepCodecWithFlush(Format oldFormat, Format newFormat) { 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 e9cb6d5a18e..e145a179d9a 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 9ecce6e1503..999059e8529 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 @@ 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 @@ private 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 @@ private 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 3de1b7b2121..e0ccd23c71d 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 dc7085c85e0..d49b4c39ca9 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 12905f908c6..1d504159e6b 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 CacheException(String message, Throwable cause) { /** * 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 CacheException(String message, Throwable cause) { * 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 CacheException(String message, Throwable cause) { * 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 CacheException(String message, Throwable cause) { * @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 CacheException(String message, Throwable cause) { /** * 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 CacheException(String message, Throwable cause) { * 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 2488ae0ff3c..978dbbc8f77 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.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 @@ /** * 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 @@ public CacheFileMetadataIndex(DatabaseProvider databaseProvider) { /** * 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 @@ public void initialize(long uid) throws DatabaseIOException { * 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 @@ public Map getAll() throws DatabaseIOException { /** * 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 @@ public void set(String name, long length, long lastTouchTimestamp) throws Databa /** * 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 @@ public void remove(String name) throws DatabaseIOException { /** * 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 bbec189b4d8..93b00718ab6 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 @@ 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 static Pair getCached( * 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 static Pair getCached( * @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 static void cache( * 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 static void cache( * @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 @@ private static long readAndDiscard( /** * 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 static void remove( /** * 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 22086f8ac8f..5ed3e921eef 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.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 @@ public static boolean isIndexFile(String fileName) { /** * 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 @@ public CachedContentIndex( /** * 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 @@ public void initialize(long uid) throws IOException { /** * 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 cf8056f5a95..a4fade25e0f 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 @@ 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 static synchronized boolean isCacheFolderLocked(File cacheFolder) { /** * 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 SimpleCache(File cacheDir, CacheEvictor evictor) { * @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 0c36bc7d0f1..6140acdff6a 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 @@ private boolean restoreKeys() { 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 4517e4ee9aa..7ed1eb095fb 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 @@ private void ensureInitialized() throws DatabaseIOException { 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 978dbbc8f77..dc27dec3638 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 @@ public static void delete(DatabaseProvider databaseProvider, long uid) 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 @@ public void initialize(long uid) throws DatabaseIOException { 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 @@ public void removeAll(Set names) throws DatabaseIOException { 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 5ed3e921eef..7e09025ddd9 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 @@ public void load( hexUid); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { initializeTable(writableDatabase); writableDatabase.setTransactionSuccessful(); @@ -832,7 +832,7 @@ public void load( 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 @@ public void storeIncremental(HashMap content) throws IOEx } 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 @@ private static void delete(DatabaseProvider databaseProvider, String hexUid) 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 57f00457917..01980c2f369 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 fb7bc4bde83..b57f0b484c5 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 6140acdff6a..a619674fa01 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 @@ public interface ReleaseCallback { 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 @@ public DefaultDrmSession( ReleaseCallback releaseCallback, @Nullable List schemeDatas, @DefaultDrmSessionManager.Mode int mode, + boolean playClearSamplesWithoutKeys, boolean isPlaceholderSession, @Nullable byte[] offlineLicenseKeySetId, HashMap keyRequestParameters, @@ -170,6 +172,7 @@ public DefaultDrmSession( 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 @@ public final int getState() { 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 f8e854e51da..1c27d745de2 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 static final class Builder { 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 Builder setUseDrmSessionsForClearContent( * @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 DefaultDrmSessionManager build(MediaDrmCallback mediaDrmC keyRequestParameters, multiSession, useDrmSessionsForClearContentTrackTypes, - flags, + playClearSamplesWithoutKeys, loadErrorHandlingPolicy); } } @@ -245,7 +241,7 @@ private MissingSchemeDataException(UUID uuid) { 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 DefaultDrmSessionManager( keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, multiSession, /* useDrmSessionsForClearContentTrackTypes= */ new int[0], - /* flags= */ 0, + /* playClearSamplesWithoutKeys= */ false, new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); } @@ -352,7 +348,7 @@ private DefaultDrmSessionManager( 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 @@ private DefaultDrmSessionManager( 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 DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa return session; } - @Override - @Flags - public final int getFlags() { - return flags; - } - @Override @Nullable public Class getExoMediaCryptoType(DrmInitData drmInitData) { @@ -571,6 +561,8 @@ private void maybeCreateMediaDrmHandler(Looper playbackLooper) { 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 @@ private DefaultDrmSession createNewDefaultSession( /* 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 13e29e141a6..6c2fdecd01c 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 DrmSessionException(Throwable cause) { */ @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 c92d68ed173..4aef7315582 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 Class getExoMediaCryptoType(DrmInitData drmInitData) { } }; - /** 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 @@ default DrmSession acquirePlaceholderSession(Looper playbackLooper, int track */ 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 aa15c829009..0028e479870 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 int getState() { 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 0cc576a145b..22061f58ef7 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 @@ public static final class SampleExtrasHolder { 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 @@ public static final class SampleExtrasHolder { 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 @@ public boolean isReady(boolean loadingFinished) { } 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 @@ public synchronized int read( 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 df6ccc1f021..441ac9e05a8 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 void testIsReadyReturnsTrueForValidDrmSession() { @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 void testReadWithErrorSessionReadsNothingAndThrows() throws IOException { @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 22061f58ef7..bb578ddec7a 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 @@ public boolean isReady(boolean loadingFinished) { 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 @@ public synchronized int read( 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 @@ private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { } } + /** + * 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 729dd150ae1..52b686000a4 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 void removeAnalyticsListener(AnalyticsListener listener) { * 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 long getContentBufferedPosition() { } /** - * 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. + * + *

        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). * - *

        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. + *

        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 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. + * @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 76abfd6e0fc..21991008cbf 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 @@ private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackExcep } 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 a619674fa01..0d93ec7c621 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 @@ private void onProvisionResponse(Object request, Object response) { @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 6c2fdecd01c..35358f04f7a 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 DrmSessionException(Throwable cause) { * 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 54e0904dab4..1361bb6ad42 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 @@ private boolean feedInputBuffer() throws ExoPlaybackException { } 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 86181664bac..73c964d1fed 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 @@ private void onOutputReset() { } 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 ff00c5b0d47..b107be4794d 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 @@ protected static String parseEac3SupplementalProperties(List supplem 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 4aef7315582..146c5d704d3 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 @@ default void release() { * 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 d92dd48d4e9..1230b45fe4e 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 boolean setReadPosition(int sampleIndex) { /** * 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 ac749316715..c175e7f240e 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 412eb58ad37..a6a65778313 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 synchronized void updateOrientation(float w, float x, float y, float z) { @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 synchronized boolean configure( 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 1bf141cb435..b4f5040be52 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 UnhandledFormatException( *

        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 a3a85bb43ad..8fcf39367b0 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 final void reset() { 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 6b84662093d..8aea8506e4f 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 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 @@ public void setChannelMap(@Nullable int[] outputChannels) { } @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 @@ public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int } 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 65d997396b6..7cdd7000580 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 long getSkippedOutputFrameCount() { 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 @@ && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) 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 @@ && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) 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 2274d53b556..1e6af46fbc2 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 @@ 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 d0c057b6769..48175049332 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 @@ /* 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 caf8a616519..35e8f8aa5f7 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 long getSkippedFrames() { // 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 bd32e5ee6f2..5bffc8fc688 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 long scaleDurationForSpeedup(long duration) { } @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 6e4c97701ae..49b17916dc7 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 TeeAudioProcessor(AudioBufferSink audioBufferSink) { } @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 c9e9f921c7c..161b7eb6523 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 @@ public long getTrimmedFrameCount() { } @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 @@ public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int 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 128591124d9..854975e6546 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 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 void testEnabledProcessor_isActive() throws Exception { 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 void testDefaultProcessor_isNotEnabled() throws Exception { 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 void testSkipInSilentSignal_skipsEverything() throws Exception { // 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 void testSkipInNoisySignal_skipsNothing() throws Exception { 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 void testSkipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCount 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 void testSkipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCo 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 void testSkipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCou 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 void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { 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 void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { 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 | 94 +++++++++++-------- .../exoplayer2/audio/BaseAudioProcessor.java | 59 +++++------- .../audio/ChannelMappingAudioProcessor.java | 45 ++++----- .../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, 259 insertions(+), 329 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 a6a65778313..7748e053b4e 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 @@ 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 synchronized void updateOrientation(float w, float x, float y, float z) { @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 synchronized void configure(int sampleRateHz, int channelCount, @C.Encodi 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 boolean isActive() { 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 void flush() { 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 synchronized void reset() { 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 b4f5040be52..1b0ddff16cb 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 @@ * 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 { + /** 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); + + /** 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 UnhandledFormatException extends Exception { + final class UnhandledAudioFormatException extends Exception { - public UnhandledFormatException( - int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " - + encoding); + public UnhandledAudioFormatException(AudioFormat inputAudioFormat) { + super("Unhandled format: " + inputAudioFormat); } } @@ -50,45 +84,23 @@ public UnhandledFormatException( /** * 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 @@ void configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) */ 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 8fcf39367b0..c9e54656441 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 boolean isActive() { - return sampleRateHz != Format.NO_VALUE; - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return encoding; + public final AudioFormat configure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + this.inputAudioFormat = inputAudioFormat; + outputAudioFormat = onConfigure(inputAudioFormat); + return isActive() ? outputAudioFormat : AudioFormat.NOT_SET; } @Override - public int getOutputSampleRateHz() { - return sampleRateHz; + public boolean isActive() { + return outputAudioFormat != AudioFormat.NOT_SET; } @Override @@ -98,20 +86,11 @@ public final void flush() { 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 @@ protected final boolean hasPendingOutput() { 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 8aea8506e4f..87be1ee88a4 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 @@ * 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 @@ public void setChannelMap(@Nullable int[] outputChannels) { } @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; + return AudioFormat.NOT_SET; } - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - setInputFormat(sampleRateHz, channelCount, encoding); - active = channelCount != outputChannels.length; + 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 @@ public void queueInput(ByteBuffer inputBuffer) { 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 @@ public void queueInput(ByteBuffer inputBuffer) { 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 7cdd7000580..27823e30069 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 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 @@ && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) 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 1e6af46fbc2..a75e675e6e0 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 @@ 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 @@ public void queueInput(ByteBuffer inputBuffer) { | ((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 48175049332..1bfa1897c85 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 @@ /* 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 @@ public void queueInput(ByteBuffer inputBuffer) { 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 @@ public void queueInput(ByteBuffer inputBuffer) { // 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 35e8f8aa5f7..59feed9bd2b 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 long getSkippedFrames() { // 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 @@ protected void onQueueEndOfStream() { @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 @@ private void updatePaddingBuffer(ByteBuffer input, byte[] buffer, int size) { * 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 5bffc8fc688..4ebadab80cf 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 @@ 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 float setPitch(float pitch) { /** * 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 void setOutputSampleRateHz(int sampleRateHz) { */ 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 boolean isEnded() { 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 void flush() { 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 49b17916dc7..652e3eea541 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 TeeAudioProcessor(AudioBufferSink audioBufferSink) { } @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 void queueInput(ByteBuffer inputBuffer) { @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 161b7eb6523..f7d75298764 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 @@ @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 @@ public TrimmingAudioProcessor() { /** * 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 @@ public long getTrimmedFrameCount() { } @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 @@ public void queueInput(ByteBuffer inputBuffer) { buffer.flip(); } - @SuppressWarnings("ReferenceEquality") @Override public ByteBuffer getOutput() { if (super.isEnded() && endBufferSize > 0) { @@ -155,7 +147,6 @@ public ByteBuffer getOutput() { 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 854975e6546..e8eb530d99c 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 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 @@ @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 void testEnabledProcessor_isActive() throws Exception { 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 void testDisabledProcessor_isNotActive() throws Exception { 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 void testDisabledProcessor_isNotActive() throws Exception { 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 void testSkipInSilentSignal_skipsEverything() throws Exception { // 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 void testSkipInNoisySignal_skipsNothing() throws Exception { // 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 void testSkipInNoisySignal_skipsNothing() throws Exception { 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 void testSkipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCount // 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 void testSkipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCount 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 void testSkipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCo // 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 void testSkipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCo 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 void testSkipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCou // 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 void testSkipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCou 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 void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { // 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 void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { 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 @@ private static long process( SilenceSkippingAudioProcessor processor, InputBufferProvider inputBufferProvider, int inputBufferSize) { + int bytesPerFrame = AUDIO_FORMAT.bytesPerFrame; processor.flush(); long totalOutputFrames = 0; while (inputBufferProvider.hasRemaining()) { @@ -260,14 +242,14 @@ private static long process( 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 @@ private static long process( * 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 837d7a97a4d..e6b448774cd 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 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 @@ @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 void setUp() { 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 void testIsActiveWithSpeedChange() throws Exception { @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 59feed9bd2b..2a98d2fb256 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 SilenceSkippingAudioProcessor() { } /** - * 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 4ebadab80cf..c683f60e762 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 SonicAudioProcessor() { } /** - * 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 float setSpeed(float speed) { 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 float setPitch(float pitch) { 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 c175e7f240e..388b6c31e25 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 7748e053b4e..8ba33290ea8 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 synchronized AudioFormat configure(AudioFormat inputAudioFormat) 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 void flush() { 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 synchronized void reset() { 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 1b0ddff16cb..f75b2cd3176 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 UnhandledAudioFormatException(AudioFormat inputAudioFormat) { * 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 c9e54656441..41cb4365042 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 @@ */ 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 BaseAudioProcessor() { @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 boolean isEnded() { public final void flush() { outputBuffer = EMPTY_BUFFER; inputEnded = false; + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; onFlush(); } @@ -86,6 +93,8 @@ public final void flush() { 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 87be1ee88a4..4fb6af1af48 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 @@ * 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 @@ public void setChannelMap(@Nullable int[] outputChannels) { @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 @@ public void queueInput(ByteBuffer inputBuffer) { 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 c683f60e762..b9a59cd620d 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 AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudio 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 boolean isEnded() { @Override public void flush() { if (isActive()) { + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; if (pendingSonicRecreation) { sonic = new Sonic( @@ -253,6 +259,8 @@ public void flush() { 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 652e3eea541..8f39dd1d85f 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 void queueInput(ByteBuffer inputBuffer) { } @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 f7d75298764..9437e4ac26c 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 @@ 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 @@ public AudioFormat onConfigure(AudioFormat inputAudioFormat) 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 @@ public void queueInput(ByteBuffer inputBuffer) { 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 @@ public void queueInput(ByteBuffer inputBuffer) { 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 @@ public boolean isEnded() { 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 388b6c31e25..4e8d986f44a 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 4e8d986f44a..884cd54a2a8 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 cf0ea79da6e..98dbef7c6c3 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 void stop() { 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 b57f0b484c5..276daae4e22 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 f8120ed11bc..fe4aca772a9 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 e534f9b2ac1..84a92f95866 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 ce88f0ef7d7..05448f20730 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 03d9b7413db..71241d9a4f7 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 884cd54a2a8..373a024eea2 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 3cd91687262..84c037a8b71 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 c458a3ea99c..c271a0af6d7 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 7fcb89dc126..4a92859b75b 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 @@ protected int supportsFormatInternal( 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 @@ protected int supportsFormatInternal( 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 4a92859b75b..c84c3b41fe1 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 LibvpxVideoRenderer( * 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 LibvpxVideoRenderer( maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys, - /* enableRowMultiThreadMode= */ false, getRuntime().availableProcessors(), /* numInputBuffers= */ 4, /* numOutputBuffers= */ 4); @@ -154,7 +152,6 @@ public LibvpxVideoRenderer( * @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 LibvpxVideoRenderer( @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, - boolean enableRowMultiThreadMode, int threads, int numInputBuffers, int numOutputBuffers) { @@ -176,7 +172,6 @@ public LibvpxVideoRenderer( maxDroppedFramesToNotify, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false, - enableRowMultiThreadMode, threads, numInputBuffers, numOutputBuffers); @@ -197,13 +192,12 @@ public LibvpxVideoRenderer( * 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 LibvpxVideoRenderer( int maxDroppedFramesToNotify, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - boolean enableRowMultiThreadMode, int threads, int numInputBuffers, int numOutputBuffers) { @@ -224,7 +217,6 @@ public LibvpxVideoRenderer( maxDroppedFramesToNotify, drmSessionManager, playClearSamplesWithoutKeys); - this.enableRowMultiThreadMode = enableRowMultiThreadMode; this.threads = threads; this.numInputBuffers = numInputBuffers; this.numOutputBuffers = numOutputBuffers; @@ -259,12 +251,7 @@ protected int supportsFormatInternal( 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 b4535a3e9c8..98a26727eec 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 @@ * @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 @@ public VpxDecoder( int numOutputBuffers, int initialInputBufferSize, @Nullable ExoMediaCrypto exoMediaCrypto, - boolean enableRowMultiThreadMode, int threads) throws VpxDecoderException { super( @@ -75,7 +73,8 @@ public VpxDecoder( 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 decb25c6660..65812e42742 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 69e8ddc52d4..f9228e4b792 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 d03d75f0777..ab47b6de811 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 355ba434058..0240a377ac3 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 1f653f160e0..bff05901b5f 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 9a4891d3296..0f3bbfa14d2 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 static void checkState(boolean expression, Object errorMessage) { } } + /** + * 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 b8642e2e42a..a6636d71bef 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 interface ProgressUpdateListener { 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 PlayerControlView(Context context, @Nullable AttributeSet attrs, int defS 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 PlayerControlView( 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 @@ private void updateNavigation() { if (!isVisible() || !isAttachedToWindow) { return; } + + @Nullable Player player = this.player; boolean enableSeeking = false; boolean enablePrevious = false; boolean enableRewind = false; @@ -809,16 +816,20 @@ private void updateRepeatModeButton() { 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 @@ private void updateShuffleButton() { if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { return; } + + @Nullable Player player = this.player; if (!showShuffleButton) { shuffleButton.setVisibility(GONE); } else if (player == null) { @@ -861,6 +874,7 @@ private void updateShuffleButton() { } private void updateTimeline() { + @Nullable Player player = this.player; if (player == null) { return; } @@ -935,6 +949,7 @@ private void updateProgress() { return; } + @Nullable Player player = this.player; long position = 0; long bufferedPosition = 0; if (player != null) { @@ -985,7 +1000,7 @@ private void requestPlayPauseFocus() { } } - private void setButtonEnabled(boolean enabled, View view) { + private void setButtonEnabled(boolean enabled, @Nullable View view) { if (view == null) { return; } @@ -1129,6 +1144,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { */ 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 2e29dd33888..c55fe09f763 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.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 PlayerView(Context context, @Nullable AttributeSet attrs) { 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 PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAtt bufferingView = null; errorMessageView = null; controller = null; - componentListener = null; adOverlayFrameLayout = null; overlayFrameLayout = null; ImageView logo = new ImageView(context); @@ -385,7 +389,6 @@ public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAtt } LayoutInflater.from(context).inflate(playerLayoutId, this); - componentListener = new ComponentListener(); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // Content frame. @@ -540,9 +543,10 @@ public void setPlayer(@Nullable Player player) { 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 void setPlayer(@Nullable Player player) { 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 void setPlayer(@Nullable Player player) { 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 void setPlayer(@Nullable Player player) { } 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 void setVisibility(int visibility) { * @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 void setUseController(boolean useController) { return; } this.useController = useController; - if (useController) { + if (useController()) { controller.setPlayer(player); } else if (controller != null) { controller.hide(); @@ -793,9 +797,9 @@ public boolean dispatchKeyEvent(KeyEvent event) { 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 boolean dispatchKeyEvent(KeyEvent event) { // 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 boolean dispatchKeyEvent(KeyEvent event) { * @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 int getControllerShowTimeoutMs() { * 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 boolean getControllerHideOnTouch() { * @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 void setControllerHideDuringAds(boolean controllerHideDuringAds) { */ public void setControllerVisibilityListener( @Nullable PlayerControlView.VisibilityListener listener) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); if (this.controllerVisibilityListener == listener) { return; } @@ -947,7 +951,7 @@ public void setControllerVisibilityListener( * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setPlaybackPreparer(playbackPreparer); } @@ -958,7 +962,7 @@ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { * DefaultControlDispatcher}. */ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { - Assertions.checkState(controller != null); + Assertions.checkStateNotNull(controller); controller.setControlDispatcher(controlDispatcher); } @@ -969,7 +973,7 @@ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) * 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 void setRewindIncrementMs(int rewindMs) { * 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 void setFastForwardIncrementMs(int fastForwardMs) { * @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 void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatTog * @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 void setShowShuffleButton(boolean showShuffleButton) { * @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 void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { */ 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 void setExtraAdGroupMarkers( */ public void setAspectRatioListener( @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { - Assertions.checkState(contentFrame != null); + Assertions.checkStateNotNull(contentFrame); contentFrame.setAspectRatioListener(listener); } @@ -1089,7 +1093,7 @@ public SubtitleView getSubtitleView() { @Override public boolean onTouchEvent(MotionEvent event) { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } switch (event.getAction()) { @@ -1116,7 +1120,7 @@ public boolean performClick() { @Override public boolean onTrackballEvent(MotionEvent ev) { - if (!useController || player == null) { + if (!useController() || player == null) { return false; } maybeShowController(true); @@ -1173,7 +1177,7 @@ protected void onContentAspectRatioChanged( @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 View[] getAdOverlayViews() { // 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 @@ private void maybeShowController(boolean isForced) { 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 @@ private boolean shouldShowControllerIndefinitely() { } private void showController(boolean showIndefinitely) { - if (!useController) { + if (!useController()) { return; } controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); @@ -1241,6 +1263,7 @@ private boolean isPlayingAd() { } private void updateForCurrentTrackSelections(boolean isNewPlayer) { + @Nullable Player player = this.player; if (player == null || player.getCurrentTrackGroups().isEmpty()) { if (!keepContentOnPlayerReset) { hideArtwork(); @@ -1267,12 +1290,12 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { // 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 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { hideArtwork(); } + @RequiresNonNull("artworkView") private boolean setArtworkFromMetadata(Metadata metadata) { boolean isArtworkSet = false; int currentPictureType = PICTURE_TYPE_NOT_SET; @@ -1316,6 +1340,7 @@ private boolean setArtworkFromMetadata(Metadata metadata) { return isArtworkSet; } + @RequiresNonNull("artworkView") private boolean setDrawableArtwork(@Nullable Drawable drawable) { if (drawable != null) { int drawableWidth = drawable.getIntrinsicWidth(); @@ -1362,13 +1387,8 @@ private void updateErrorMessage() { 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 @@ private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, in /** 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 @@ private static void applyTextureViewRotation(TextureView textureView, int textur 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 0d74652408b..437546559c3 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 static final class ParametersBuilder extends TrackSelectionParameters.Bui private boolean exceedRendererCapabilitiesIfNecessary; private int tunnelingAudioSessionId; - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; /** @@ -646,8 +647,9 @@ public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean di * @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 final ParametersBuilder setSelectionOverride( */ 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 final ParametersBuilder clearSelectionOverride( * @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 @@ private void setInitialValuesWithoutContext(@UnderInitialization ParametersBuild 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 static Parameters getDefaults(Context context) { public final int tunnelingAudioSessionId; // Overrides - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; /* package */ Parameters( @@ -996,7 +1003,7 @@ public static Parameters getDefaults(Context context) { boolean exceedRendererCapabilitiesIfNecessary, int tunnelingAudioSessionId, // Overrides - SparseArray> selectionOverrides, + SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags) { super( preferredAudioLanguage, @@ -1087,7 +1094,8 @@ public final boolean getRendererDisabled(int rendererIndex) { * @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 final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray gro */ @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 Parameters[] newArray(int size) { // 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 @@ private static SparseArray> readSelectio } 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 @@ private static boolean areRendererDisabledFlagsEqual( } 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 @@ private static boolean areSelectionOverridesEqual( } 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 final boolean getRendererDisabled(int rendererIndex) { */ @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 894736571ca..52bf4b3d061 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.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 @@ */ 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 @@ protected void onReset() { // 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 042738b4f68..bf5822caf61 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 EventDispatcher(@Nullable Handler handler, * 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 void enabled(final DecoderCounters decoderCounters) { */ 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 void decoderInitialized(final String decoderName, * 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 void inputFormatChanged(final Format format) { */ 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 void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, */ 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 void disabled(final DecoderCounters counters) { * 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 e454bd51c89..8412b738bbb 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 @@ 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 static boolean populateHeader(int headerData, MpegAudioHeader header) { /** 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 @@ private void setValues( 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 bbcb75aa2d9..97ce0c6a1ec 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 @@ * @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 ae4b7db5c93..0b653830a37 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 boolean supportsFormat(Format format) { @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 a64a1835d85..927ee8be5ea 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 boolean supportsFormat(Format format) { @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 616859f0475..a498f510dd5 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 boolean isRetry() { 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 void clearFatalError() { */ 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 @@ private final class LoadTask extends Handler implements Runn 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 void cancel(boolean released) { } else { canceled = true; loadable.cancelLoad(); + Thread executorThread = this.executorThread; if (executorThread != null) { executorThread.interrupt(); } @@ -375,7 +371,8 @@ public void cancel(boolean released) { 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 void handleMessage(Message msg) { 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 void handleMessage(Message msg) { 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 93b00718ab6..ce16ea24397 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 static void cache( @Nullable CacheKeyFactory cacheKeyFactory, CacheDataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, @@ -268,11 +268,11 @@ private static long readAndDiscard( 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 @@ private static String buildCacheKey( .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 44a735f144d..c88e2643d8a 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 @@ 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 void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) { 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 @@ private void evictCache(Cache cache, long requiredSpace) { } } + 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 7d9f0c9ff12..5f6ea338e67 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 @@ public static SimpleCacheSpan createCacheEntry( 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 @@ private static File upgradeFile(File file, CachedContentIndex index) { 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 70f30d32804..e7dfd123b14 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 EventDispatcher(@Nullable Handler handler, /** 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 void videoSizeChanged( 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 1efdeac84d8..8b737bc0066 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 @@ */ 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 373a024eea2..d0ae1a3b5af 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 381f19809bf..5a0973188b3 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.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 @@ private int peekId3Header(ExtractorInput input) throws IOException, InterruptedE } scratch.skipBytes(3); int length = scratch.readSynchSafeInt(); - firstFramePosition += 10 + length; + firstFramePosition += ID3_HEADER_LENGTH + length; input.advancePeekPosition(length); } input.resetPeekPosition(); @@ -266,36 +267,43 @@ private void calculateAverageFrameSize(ExtractorInput input) 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 5a0973188b3..86dacd8c30b 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 @@ private void calculateAverageFrameSize(ExtractorInput input) } 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 d0ae1a3b5af..1911d32cdd3 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 84d5fea0c70..5382e286a18 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 interface RatingCallback extends CommandReceiver { 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 interface MediaMetadataProvider { @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 void setQueueEditor(@Nullable QueueEditor queueEditor) { * * @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 void setRatingCallback(RatingCallback ratingCallback) { } } + /** + * 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 @@ private long buildPlaybackActions(Player player) { 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 @@ private long buildPlaybackActions(Player player) { 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 @@ private boolean canDispatchSetRating() { 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 void onRemoveQueueItem(MediaDescriptionCompat description) { } } + @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 1911d32cdd3..05744456003 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 ffabbece97a..993ce8e5c1a 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 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri } else if (line.startsWith(TAG_STREAM_INF)) { noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); - String averageBandwidthString = - parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH, variableDefinitions); - if (averageBandwidthString != null) { - // If available, the average bandwidth attribute is used as the variant's bitrate. - bitrate = Integer.parseInt(averageBandwidthString); - } + // TODO: Plumb this into Format. + int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1); String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions); String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions); @@ -869,6 +865,14 @@ private static int parseIntAttr(String line, Pattern pattern) throws ParserExcep return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap())); } + private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + return defaultValue; + } + private static long parseLongAttr(String line, Pattern pattern) throws ParserException { return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap())); } 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 254a2b2bd16..0aa78d9f026 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 @@ -243,7 +243,7 @@ public void testMasterPlaylistWithBandwdithAverage() throws IOException { List 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 a5d763ca720..bd617ad6265 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 569fc934565..6c77284e46f 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.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.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 @@ private BitmapCallback(int notificationTag) { */ 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 void onBitmap(final Bitmap bitmap) { */ 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 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 final void setPlayer(@Nullable Player player) { this.player = player; if (player != null) { player.addListener(playerListener); - startOrUpdateNotification(); + postStartOrUpdateNotification(); } } @@ -945,26 +946,17 @@ public final void setVisibility(@Visibility int visibility) { /** 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 @@ private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { 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 @@ private boolean shouldShowPauseButton(Player player) { && 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 @@ private class PlayerListener implements Player.EventListener { @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 | 381 ++++++++++++++---- .../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, 917 insertions(+), 96 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 05744456003..b1b39b75b1d 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 65812e42742..599af54dde2 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 e145a179d9a..3cc14326c5a 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 2e78b433bd9..d7517728791 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.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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { 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; } } } /** - * Parses the event body of the subtitle. + * Parse the {@code [Script Info]} section. * - * @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. + *

        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 parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { + private void parseScriptInfo(ParsableByteArray data) { String currentLine; - while ((currentLine = data.readLine()) != null) { - if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { - parseFormatLine(currentLine); - } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { - parseDialogueLine(currentLine, cues, cueTimesUs); + 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; } } } /** - * Parses a format line. + * Parse the {@code [V4+ Styles]} section. * - * @param formatLine The line to parse. + *

        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 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; + 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); + } } } - 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; - } + return styles; } /** - * Parses a dialogue line. + * Parses the event body of the subtitle. * - * @param dialogueLine The line to parse. + * @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 parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { - if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); - return; + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; + String currentLine; + while ((currentLine = data.readLine()) != null) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + format = SsaDialogueFormat.fromFormatLine(currentLine); + } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { + if (format == null) { + Log.w(TAG, "Skipping dialogue line before complete format: " + currentLine); + continue; + } + parseDialogueLine(currentLine, format, cues, cueTimesUs); + } } + } - String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) - .split(",", formatKeyCount); - if (lineValues.length != formatKeyCount) { + /** + * Parses a dialogue line. + * + * @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 A sorted list to which parsed cue timestamps will be added. + */ + 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 @@ private void parseDialogueLine(String dialogueLine, List cues, LongArray cu * @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 static long parseTimecodeUs(String timeString) { 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 00000000000..03c025cd94a --- /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 00000000000..e8070976e72 --- /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 9a3756194f0..4093f7974dd 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 @@ */ /* 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 @@ public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { @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 00000000000..ade4cce9c47 --- /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 00000000000..2093a96ac55 --- /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 00000000000..af19fc37241 --- /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 00000000000..75b7967b345 --- /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 4542af1217c..3d36503251f 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 3c48aa61dd2..9112bec398f 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 void testDecodeTypical() throws IOException { 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 void testDecodeTypicalWithInitializationData() throws IOException { 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 45650c45fa7..1b492e38c7a 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 @@ 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 @@ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput * @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 @@ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput /** * 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 @@ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput * @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 @@ boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput 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 @@ boolean advancePeekPosition(int length, boolean allowEndOfInput) 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 35702da576c..046c1fef556 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.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 @@ default byte[] getWrappedMetadataBytes() { * @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 Metadata copyWithAppendedEntriesFrom(@Nullable Metadata other) { * @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 d738a8662e5..5b287b04148 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 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 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 int supportsFormat(Format format) { } @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 @@ protected void onPositionReset(long positionUs, boolean joining) { } @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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx } 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx } } } 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx */ 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 @@ private void decodeWrappedMetadata(Metadata metadata, List decod 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 00000000000..2b03ce8df32 --- /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 00000000000..343ab232e0a --- /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 13d6b485b37..3834dce583e 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 @@ /** 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 Metadata decode(MetadataInputBuffer inputBuffer) { @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 00000000000..2a2d0c7fc26 --- /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 d4bedc63cc4..3f4a4006775 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 ApicFrame( /* 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 ba0968cbd44..faab7f0775a 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 Metadata decode(byte[] data, int size) { * @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 @@ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersio } } - private static @Nullable Id3Frame decodeFrame( + @Nullable + private static Id3Frame decodeFrame( int majorVersion, ParsableByteArray id3Data, boolean unsignedIntFrameSizeHack, @@ -404,8 +406,9 @@ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersio } } - 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 @@ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersio 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 @@ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersio 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 @@ private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSiz 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 00000000000..84220718426 --- /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 00000000000..a55cc1b6b3f --- /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 1153f918fc4..0e161d9c692 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 SpliceInfoDecoder() { @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 Metadata decode(MetadataInputBuffer inputBuffer) { 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 Metadata decode(MetadataInputBuffer inputBuffer) { 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 00000000000..0c4448f4d3c --- /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 81cfec29fd5..3d10c2579b5 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.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 Libgav1VideoRenderer( } @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 39d1ee4094f..17292cec341 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 FfmpegAudioRenderer( } @Override + @FormatSupport protected int supportsFormatInternal( @Nullable DrmSessionManager drmSessionManager, Format format) { Assertions.checkNotNull(format.sampleMimeType); @@ -108,6 +109,7 @@ protected int supportsFormatInternal( } @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 d833c47d140..3e8d727476e 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 LibflacAudioRenderer( } @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 d17b6ebb535..3592331eff8 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 LibopusAudioRenderer( } @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 c84c3b41fe1..28cb35e60fc 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.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 LibvpxVideoRenderer( } @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 @@ protected int supportsFormatInternal( || (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 3cc14326c5a..6e512e4c1ed 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 3cdab8baf16..bf43e74c2a4 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 final void reset() { // 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 52bf4b3d061..b0f690d3e7b 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 boolean isEnded() { // 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 de0d481386b..95f1749f10b 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 @@ 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 a6a8b034482..3e48966c54d 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.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 MediaCodecAudioRenderer( } @Override + @Capabilities protected int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, @@ -365,8 +367,9 @@ protected int supportsFormat( 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 @@ protected int supportsFormat( 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 21991008cbf..d5a5ffe7bbc 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.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 MediaClock getMediaClock() { } @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 1361bb6ad42..e8501dad757 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 void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) { } @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 final int supportsFormat(Format format) throws ExoPlaybackException { } /** - * 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 5b287b04148..7a5235a4667 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.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 MetadataRenderer( } @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 1622d68d991..35e60dcf82b 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.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 TextRenderer( } @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 437546559c3..9982ce53699 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.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 void experimental_allowMultipleAdaptiveSelections() { 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 void experimental_allowMultipleAdaptiveSelections() { * 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 void experimental_allowMultipleAdaptiveSelections() { * {@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 void experimental_allowMultipleAdaptiveSelections() { @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 @@ protected TrackSelection.Definition selectVideoTrack( @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 @@ private static TrackSelection.Definition selectAdaptiveVideoTrack( private static int[] getAdaptiveVideoTracksForGroup( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, @@ -1926,7 +1929,7 @@ private static int[] getAdaptiveVideoTracksForGroup( private static int getAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1954,7 +1957,7 @@ private static int getAdaptiveVideoTrackCountForMimeType( private static void filterAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1981,7 +1984,7 @@ private static void filterAdaptiveVideoTrackCountForMimeType( private static boolean isSupportedAdaptiveVideoTrack( Format format, @Nullable String mimeType, - int formatSupport, + @Capabilities int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, @@ -1998,7 +2001,7 @@ private static boolean isSupportedAdaptiveVideoTrack( @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 @@ private static TrackSelection.Definition selectFixedVideoTrack( 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 @@ private static TrackSelection.Definition selectFixedVideoTrack( * {@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 @@ private static TrackSelection.Definition selectFixedVideoTrack( @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 @@ protected Pair selectAudioTrack( 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 @@ protected Pair selectAudioTrack( private static int[] getAdaptiveAudioTracks( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, boolean allowMixedSampleRateAdaptiveness, @@ -2202,7 +2205,7 @@ private static int[] getAdaptiveAudioTracks( private static int getAdaptiveAudioTrackCount( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2226,7 +2229,7 @@ private static int getAdaptiveAudioTrackCount( private static boolean isSupportedAdaptiveAudioTrack( Format format, - int formatSupport, + @Capabilities int formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2252,8 +2255,8 @@ private static boolean isSupportedAdaptiveAudioTrack( * {@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 @@ private static boolean isSupportedAdaptiveAudioTrack( @Nullable protected Pair selectTextTrack( TrackGroupArray groups, - int[][] formatSupport, + @Capabilities int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { @@ -2273,7 +2276,7 @@ protected Pair selectTextTrack( 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 @@ protected Pair selectTextTrack( * * @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 @@ protected TrackSelection.Definition selectOtherTrack( * 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 @@ protected TrackSelection.Definition selectOtherTrack( */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - int[][][] renderererFormatSupports, + @Capabilities int[][][] renderererFormatSupports, @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { @@ -2408,21 +2413,22 @@ private static void maybeConfigureRenderersForTunneling( /** * 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 @@ private static int compareFormatValues(int first, int second) { } /** - * 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 @@ protected static final class AudioTrackScore implements Comparable selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** @@ -446,12 +452,14 @@ public final TrackSelectorResult selectTracks( 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 @@ private static int findRenderer(RendererCapabilities[] rendererCapabilities, Tra } /** - * 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 @@ private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, * 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 a4e8e311ca2..6caf549afe5 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.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 void onTracksChanged( 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 @@ private static String getStateString(int state) { } } - 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 @@ private static String getFormatSupportString(int formatSupport) { 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 @@ private static String getAdaptiveSupportString(int trackCount, int adaptiveSuppo 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 38ac80bf263..57c3ab13fa7 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.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 MediaCodecVideoRenderer( } @Override + @Capabilities protected int supportsFormat( MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, @@ -367,7 +369,7 @@ protected int supportsFormat( 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 @@ protected int supportsFormat( /* requiresTunnelingDecoder= */ false); } if (decoderInfos.isEmpty()) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } boolean supportsFormatDrm = drmInitData == null @@ -396,16 +398,17 @@ protected int supportsFormat( || (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 @@ protected int supportsFormat( } } } + @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 73c964d1fed..9aa50e4388d 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 @@ protected SimpleDecoderVideoRenderer( // BaseRenderer implementation. @Override + @Capabilities public final int supportsFormat(Format format) { return supportsFormatInternal(drmSessionManager, format); } @@ -498,13 +499,14 @@ protected void updateDroppedBufferCounters(int droppedBufferCount) { } /** - * 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 d1cf0abc567..35804adbe3f 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.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 CameraMotionRenderer() { } @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 6769f5049b4..f8fd2fc9cad 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 void setUp() throws Exception { 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 292742b5279..62d38187c4e 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 @@ private static Format buildTextFormat(String id, String language, int selectionF 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 @@ private static final class FakeRendererCapabilities implements RendererCapabilit * 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 int getTrackType() { } @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 int getTrackType() { } @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 efb828fc578..f7bfc24881f 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.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 @@ private static final class FakeMappingTrackSelector extends MappingTrackSelector @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 int getTrackType() { } @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 79990e53a62..1e2d226fd6c 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 @@ private void onTrackViewClicked(View view) { 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 8323d666149..5deed11699f 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 @@ private static int getTrackIndex(TrackGroup trackGroup, String formatId) { } 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 39d3d8f7f40..987a9e33c15 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.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 boolean isEnded() { } @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 3f2fef454fa..0e41e4d1b6a 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 @@ private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, 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 0d93ec7c621..432cc6613f3 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.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 @@ public interface ReleaseCallback { @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 @@ public interface ReleaseCallback { * @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 8412b738bbb..04d85b8bc5f 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 bd617ad6265..946af76e536 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 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 bd561afaf8b..8a1aea179ae 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 @@ 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 @@ protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) { 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 35e60dcf82b..d359eebfdbd 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 int supportsFormat(Format format) { } @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 fc1f0e2bdc2..e04094a8dc6 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 @@ */ /* 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 @@ public int compareTo(@NonNull Cea708Cue other) { } 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 b3be88b8511..4391bc0bf02 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.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 00000000000..cbdf178b6ab --- /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 0e41e4d1b6a..8382d9d9d08 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.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 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 @@ 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 @@ public List decode(byte[] data, int limit) { parseSubtitlingSegment(dataBitArray, subtitleService); } - if (subtitleService.pageComposition == null) { + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { return Collections.emptyList(); } @@ -147,7 +150,7 @@ public List decode(byte[] data, int limit) { // 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 @@ public List decode(byte[] data, int limit) { 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 @@ private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleServic 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 @@ private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleServic } 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 @@ private static ObjectData parseObjectData(ParsableBitArray data) { 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 @@ private static int getColor(int a, int r, int g, int b) { // 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 @@ private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinitio 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 @@ private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, 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 @@ private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, 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 @@ private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, } } - /** - * 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 @@ private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEnt 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 @@ private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEnt 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 @@ private static final class SubtitleService { 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 @@ public RegionComposition(int id, boolean fillFlag, int width, int height, } 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 00000000000..e5ec87a1a5d --- /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 00000000000..5c5b3bbc31a --- /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 d7517728791..45d4554bb75 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { * @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 @@ private void parseHeader(ParsableByteArray data) { * 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 @@ private void parseScriptInfo(ParsableByteArray data) { * 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 @@ private static Map parseStyles(ParsableByteArray data) { 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 @@ private static Map parseStyles(ParsableByteArray data) { * @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 @@ private void parseDialogueLine( return; } + @Nullable SsaStyle style = styles != null && format.styleIndex != C.INDEX_UNSET ? styles.get(lineValues[format.styleIndex].trim()) @@ -281,10 +285,11 @@ private static long parseTimecodeUs(String timeString) { 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 00000000000..cdf891d0168 --- /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 20b7efe50a7..0c402ac018c 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { 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 @@ protected Subtitle decode(byte[] bytes, int length, boolean reset) { 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 00000000000..5b0685e24c8 --- /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 91864557023..97c0acb1ec3 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 String getFontFamily() { 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 Layout.Alignment getTextAlign() { 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 eae879c21bb..bfa067e322e 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 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 f587d70e903..6e5bd31b4b6 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.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 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 boolean parseCue( 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 dce8f8157f4..90750831110 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 static void validateWebvttHeaderLine(ParsableByteArray input) throws Pars * @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 static float parsePercentage(String s) throws NumberFormatException { */ @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 e8501dad757..50b3ab8a0e2 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 b1b39b75b1d..c9564e2d582 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 bf43e74c2a4..10573af4199 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 @@ protected final DrmSession getUpdatedSourceDrmSess @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 @@ protected final int getIndex() { 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 @@ protected final int getIndex() { * * @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 49aacd9638e..653b6002d91 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 @@ 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 static ExoPlaybackException createForSource(IOException cause) { * * @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 static ExoPlaybackException createForRenderer(Exception cause, int render * @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 static ExoPlaybackException createForRemote(String message) { * @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) { + this( + type, + cause, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); } - private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { + 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 @@ private ExoPlaybackException(@Type int type, String message) { 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 4c25c180f43..240c6436c48 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 @@ public boolean handleMessage(Message msg) { } 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 @@ public boolean handleMessage(Message msg) { // 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 95f1749f10b..a75765262b8 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 @@ static int getTunnelingSupport(@Capabilities int supportFlags) { 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 3e48966c54d..ae50d14728d 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 @@ protected void onCodecInitialized(String name, long initializedTimestampMs, @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 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFo 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 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFo } 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 @@ protected boolean processOutputBuffer( 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 @@ protected void renderToEndOfStream() throws ExoPlaybackException { 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 @@ private static boolean codecNeedsEosBufferTimestampWorkaround(String codecName) || 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 d5a5ffe7bbc..5ccbf04c5cd 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return; } @@ -300,7 +300,7 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 @@ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackExc } @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 @@ private void processEndOfStream() throws ExoPlaybackException { 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 @@ private void maybeInitDecoder() throws ExoPlaybackException { 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 50b3ab8a0e2..90b1d4286e6 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 final int supportsFormat(Format format) throws ExoPlaybackException { try { return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, format); } } @@ -538,7 +538,7 @@ protected final void maybeInitCodec() throws ExoPlaybackException { 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 @@ protected final void maybeInitCodec() throws ExoPlaybackException { 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 @@ protected final void maybeInitCodec() throws ExoPlaybackException { try { maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); } catch (DecoderInitializationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -722,8 +722,7 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx 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 @@ private boolean feedInputBuffer() throws ExoPlaybackException { resetInputBuffer(); } } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return false; } @@ -1186,7 +1185,7 @@ private boolean feedInputBuffer() throws ExoPlaybackException { codecReconfigurationState = RECONFIGURATION_STATE_NONE; decoderCounters.inputBufferCount++; } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return true; } @@ -1199,7 +1198,7 @@ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackExc } @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 @@ private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackExceptio 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 d359eebfdbd..058b1c4526b 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, streamFormat); } } @@ -247,7 +247,7 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx } } } 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 6caf549afe5..0a303c1df7c 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.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 void onTracksChanged( 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 void onTracksChanged( 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 void onMetadata(EventTime eventTime, Metadata metadata) { @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 void onVolumeChanged(EventTime eventTime, float volume) { @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 void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Form 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 @@ private static String getStateString(int state) { } } - 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 @@ private static String getTimelineChangeReasonString(@Player.TimelineChangeReason } } - 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 23447acddf4..c8a947e7d68 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 static RendererCapabilities[] getRendererCapabilities( 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 9aa50e4388d..bf0a28ffa0c 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 void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx while (feedInputBuffer()) {} TraceUtil.endSection(); } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -681,7 +681,7 @@ private void maybeInitDecoder() throws ExoPlaybackException { decoderInitializedTimestamp - decoderInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (VideoDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -887,7 +887,7 @@ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackExc } @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 16f5b1fb29c..ad58e832aae 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 @@ private boolean readAtomHeader(ExtractorInput input) throws IOException, Interru 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 69bdb2cd464..ff64357ca79 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 MatroskaExtractor(@Flags int flags) { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); - blockAddData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); } @Override @@ -446,9 +448,9 @@ public final void release() { @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 @@ protected void startMasterElement(int id, long contentPosition, long contentSize } 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 @@ protected void endMasterElement(int id) throws ParserException { 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 @@ protected void integerElement(int id, long value) throws ParserException { 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 @@ protected void integerElement(int id, long value) throws ParserException { } break; case ID_BLOCK_ADD_ID: - blockAddId = (int) value; + blockAdditionalId = (int) value; break; default: break; @@ -1199,7 +1201,8 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) } 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 @@ private void commitSampleToOutput(Track track, long timeUs) { 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 @@ private void writeSampleData(ExtractorInput input, Track track, int size) 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 ff64357ca79..31f9f324849 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) // 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 c8a947e7d68..0ee52dba2b2 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 static Point getPhysicalDisplaySize(Context context) { * @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 c9564e2d582..83ccb1be163 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 1b492e38c7a..461b059bad9 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 @@ * 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 @@ * 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 @@ boolean advancePeekPosition(int length, boolean allowEndOfInput) 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 45d4554bb75..917ac8e36ee 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 SsaDecoder() { } /** - * 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 | 181 ++++++++++-------- 1 file changed, 106 insertions(+), 75 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 31f9f324849..0b7b5bd053f 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 void seek(long position, long timeUs) { 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 @@ protected void endMasterElement(int id) throws ParserException { 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 @@ protected void handleBlockAdditionalData( } } - 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 @@ private void commitSampleToOutput(Track track, long timeUs) { // 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 @@ private void readScratch(ExtractorInput input, int requiredLength) 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 @@ private void writeSampleData(ExtractorInput input, Track track, int size) 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 @@ private void writeSampleData(ExtractorInput input, Track track, int size) 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 @@ private void writeSampleData(ExtractorInput input, Track track, int size) 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 @@ private static byte[] formatSubtitleTimecode( 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 @@ private static byte[] formatSubtitleTimecode( * 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 @@ private static final class TrueHdSampleRechunker { 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 TrueHdSampleRechunker() { 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; - chunkSize = 0; + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; } - chunkSize += size; + foundSyncframe = true; } - public void sampleMetadata(Track track, long timeUs) { + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { if (!foundSyncframe) { return; } - if (sampleCount++ == 0) { - // This is the first sample in the chunk, so update the timestamp. - this.timeUs = timeUs; + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; + chunkSize = 0; } - if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + chunkSize += size; + 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 83ccb1be163..19a78687273 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 0b7b5bd053f..403f6c3d419 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 @@ protected void endMasterElement(int id) throws ParserException { // 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) } 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 @@ protected void binaryElement(int id, int contentSize, ExtractorInput input) } 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 @@ private void commitSampleToOutput( 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 @@ private void commitSampleToOutput( } 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 dc27dec3638..e288a5258ea 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 @@ 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 00000000000..283487f7ea2 --- /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 19a78687273..67a5ba083d8 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 67a5ba083d8..5add56ca086 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 90b1d4286e6..820f9f003ea 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 @@ private static String getDiagnosticInfoV21(Throwable cause) { @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 @@ private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exce 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 54b5baea57d..26d29d71f69 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 FakeAdaptiveMediaPeriod( 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 long selectTracks( 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 @@ protected SampleStream createSampleStream(TrackSelection trackSelection) { 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 bb8226e172c..88de84603e8 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 @@ private static Format buildCea608TrackFormat( /* 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 42ac82e5530..f7940fed1b6 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 @@ private static TrackGroupArray buildTrackGroups( 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 26d29d71f69..011270d543f 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 FakeAdaptiveMediaPeriod( 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 long selectTracks( 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 @@ protected SampleStream createSampleStream(TrackSelection trackSelection) { 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]; + } }